SPB Git

spb/qwhpi Public

QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.

Python 63.9% TypeScript 25.4% CSS 5.5% TeX 3.5% SQL 0.8% Makefile 0.5% Dockerfile 0.5%

Initial commit — QHPI platform (engine, API, dashboard, paper)

Quebec Housing Price Index v2.1: hedonic rolling-time-dummy engine with
hierarchical pooling, FastAPI service, Next.js dashboard, validation suite
and methodology paper. Aggregated series only — no individual transaction
data is versioned (enforced by .gitignore).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed yesterday (Aug 9, 2026)

Showing 164 changed files with +19,433 and −0

added .gitignore +55 −0
@@ -0,0 +1,55 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : .gitignore
6 +# Purpose : Keep transaction-level data, build artifacts and caches out of git.
7 +# =============================================================================
8 +
9 +# ---------------------------------------------------------------------------
10 +# DATA POLICY — individual transaction data NEVER enters the repository.
11 +# Only aggregated, published series (indexes, liquidity, coverage, vintages)
12 +# are versioned. See README § Data policy.
13 +# ---------------------------------------------------------------------------
14 +data/raw/
15 +data/interim/
16 +data/processed/transactions_clean.parquet
17 +data/processed/geo_join.parquet
18 +
19 +# Large external boundary files (documented in data/external/SDA_SOURCE.md)
20 +data/external/*.gpkg
21 +data/external/*.gpkg.zip
22 +
23 +# Python
24 +__pycache__/
25 +*.py[cod]
26 +*.egg-info/
27 +.venv/
28 +venv/
29 +.pytest_cache/
30 +.mypy_cache/
31 +.ruff_cache/
32 +
33 +# Node / Next.js
34 +node_modules/
35 +web/.next/
36 +web/out/
37 +*.tsbuildinfo
38 +web/next-env.d.ts
39 +
40 +# LaTeX build artifacts (keep qwhpi.tex, references.bib, qwhpi.pdf)
41 +paper/*.aux
42 +paper/*.log
43 +paper/*.out
44 +paper/*.fls
45 +paper/*.fdb_latexmk
46 +paper/*.blg
47 +paper/*.bbl
48 +paper/*.toc
49 +
50 +# OS / editor / local
51 +.DS_Store
52 +.claude/
53 +.env
54 +.env.*
55 +*.local.json
added .pre-commit-config.yaml +22 −0
@@ -0,0 +1,22 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : .pre-commit-config.yaml
6 +# Purpose : Pre-commit hooks — mandatory header check, lint, type hygiene.
7 +# =============================================================================
8 +
9 +repos:
10 + - repo: local
11 + hooks:
12 + - id: qwhpi-headers
13 + name: QWHPI author-header check
14 + entry: python3 scripts/check_headers.py
15 + language: system
16 + pass_filenames: true
17 + types_or: [python, ts, tsx, javascript, css, sql, yaml, toml, shell]
18 + - repo: https://github.com/astral-sh/ruff-pre-commit
19 + rev: v0.6.9
20 + hooks:
21 + - id: ruff
22 + args: [--fix]
added CLAUDE.md +325 −0
@@ -0,0 +1,325 @@
1 +# CLAUDE.md — QWHPI Platform
2 +# Quebec Weekly Housing Price Index — Full Platform Build
3 +
4 +---
5 +
6 +## 0. MANDATORY FILE HEADERS — READ FIRST
7 +
8 +Every single file created in this project — Python, TypeScript, JavaScript, SQL, YAML, TOML, shell scripts, LaTeX, CSS, Dockerfiles, config files, notebooks — MUST begin with an author header.
9 +
10 +**Python / shell / YAML / TOML / Dockerfile:**
11 +
12 +```python
13 +# =============================================================================
14 +# QWHPI — Quebec Weekly Housing Price Index
15 +# Author : Simon-Pierre Boucher
16 +# Contact : contact@spboucher.ai
17 +# File : <relative/path/to/file.py>
18 +# Purpose : <one-line description>
19 +# =============================================================================
20 +```
21 +
22 +**TypeScript / JavaScript / TSX / CSS:**
23 +
24 +```ts
25 +/**
26 + * =============================================================================
27 + * QWHPI — Quebec Weekly Housing Price Index
28 + * Author : Simon-Pierre Boucher
29 + * Contact : contact@spboucher.ai
30 + * File : <relative/path/to/file.ts>
31 + * Purpose : <one-line description>
32 + * =============================================================================
33 + */
34 +```
35 +
36 +**SQL:**
37 +
38 +```sql
39 +-- =============================================================================
40 +-- QWHPI — Quebec Weekly Housing Price Index
41 +-- Author : Simon-Pierre Boucher
42 +-- Contact : contact@spboucher.ai
43 +-- File : <relative/path/to/file.sql>
44 +-- Purpose : <one-line description>
45 +-- =============================================================================
46 +```
47 +
48 +**Markdown / LaTeX:** include an equivalent comment or front-matter block at the top.
49 +
50 +Rules:
51 +- No file ships without this header. Add it at file creation time, not as a cleanup pass.
52 +- The `File` and `Purpose` lines must be accurate and kept up to date.
53 +- Add a pre-commit hook (`scripts/check_headers.py`) that fails CI if any tracked source file is missing the header.
54 +- API responses and the frontend footer must credit: **Simon-Pierre Boucher — contact@spboucher.ai**.
55 +
56 +---
57 +
58 +## 1. Mission
59 +
60 +Build a complete, production-grade **platform** around the Quebec Weekly Housing Price Index — not just a research pipeline. The platform has four layers:
61 +
62 +1. **Index Engine** — reproducible econometric pipeline producing quality-adjusted weekly indexes from `province_transactions.csv`.
63 +2. **Data Store** — versioned Parquet lake + relational database serving the API.
64 +3. **API** — FastAPI service exposing every published series with metadata, uncertainty, and reliability.
65 +4. **Dashboard** — interactive web frontend (charts, maps, comparisons, downloads).
66 +
67 +The index must NOT be a median-price tracker. It is a **hierarchical hedonic time-dummy index**: pooled hedonic estimation of characteristics, plus a latent weekly market state per `geography × property_type`, with partial pooling/shrinkage toward regional trends when weekly liquidity is thin.
68 +
69 +Target queryable observation:
70 +
71 +```
72 +Geography: Quebec City | Type: Condo | Week: 2024-05-06
73 +Index: 137.42 | 1w: +0.31% | 4w: +1.18% | YoY: +6.74%
74 +Representative value: $389,200 | Transactions: 47 | Reliability: A
75 +```
76 +
77 +---
78 +
79 +## 2. Dataset
80 +
81 +`province_transactions.csv` — ~745,119 transactions, 291 weeks, 2021-01-04 → 2026-07-27, 1,100+ cities.
82 +
83 +Columns: `id, date, amount, street, zipCode, city, lat, lng, propertyType, yearBuilt, floorArea, buildingType, previousValue, totalArValue, ownerType`
84 +
85 +Property types: `unifamilial` (~443k), `condo` (~100k), `plex` (~94k), `indéterminé` (~108k). `indéterminé` must never contaminate type-specific indexes; study what it represents and document it in an appendix.
86 +
87 +Several hedonic characteristics have missing values. Handle missingness explicitly — never `dropna()` the sample.
88 +
89 +---
90 +
91 +## 3. Repository Structure (Monorepo)
92 +
93 +```
94 +qwhpi-platform/
95 +├── CLAUDE.md
96 +├── README.md
97 +├── Makefile # one-command targets: make pipeline, make api, make web
98 +├── docker-compose.yml # db + api + web + scheduler
99 +├── .pre-commit-config.yaml
100 +
101 +├── engine/ # Index Engine (Python)
102 +│ ├── pyproject.toml
103 +│ ├── src/qwhpi/
104 +│ │ ├── config.py ingest.py clean.py geography.py features.py
105 +│ │ ├── hedonic.py hierarchy.py state_space.py repeat_sales.py
106 +│ │ ├── index.py uncertainty.py reliability.py validation.py
107 +│ │ ├── seasonal.py nowcast.py vintages.py export.py
108 +│ ├── scripts/ # 01_profile … 10_build_report (ordered, idempotent)
109 +│ ├── tests/
110 +│ └── notebooks/exploratory_only/
111 +
112 +├── data/
113 +│ ├── raw/ external/ interim/ processed/ # raw is immutable
114 +
115 +├── db/
116 +│ ├── migrations/ # Alembic
117 +│ └── schema.sql
118 +
119 +├── api/ # FastAPI service
120 +│ ├── pyproject.toml
121 +│ ├── app/
122 +│ │ ├── main.py deps.py models.py schemas.py
123 +│ │ ├── routers/ (index.py, geographies.py, liquidity.py, maps.py, meta.py)
124 +│ │ └── services/
125 +│ └── tests/
126 +
127 +├── web/ # Frontend (Next.js + TypeScript)
128 +│ ├── package.json
129 +│ ├── app/ # routes: /, /explore, /compare, /map, /methodology, /api-docs
130 +│ ├── components/ lib/ styles/
131 +│ └── tests/
132 +
133 +├── ops/
134 +│ ├── scheduler/ # weekly refresh job
135 +│ └── ci/
136 +
137 +├── outputs/ # figures, tables, maps, reports
138 +├── paper/ # LaTeX methodology paper
139 +└── scripts/check_headers.py
140 +```
141 +
142 +Production logic lives in `engine/src`, `api/app`, `web/` — never scattered in notebooks.
143 +
144 +---
145 +
146 +## 4. Index Engine — Methodology (non-negotiable core)
147 +
148 +### 4.1 Weekly calendar
149 +Monday→Sunday weeks, each labeled by its Monday. Continuous grid — never drop zero-transaction weeks; flag them.
150 +
151 +### 4.2 Geography
152 +Hierarchy: Quebec → 17 administrative regions → municipality → (optional) local zone. Spatial-join `lat/lng` against an authoritative Quebec/StatCan boundary dataset (document source, URL, version, CRS). Persist the join once in `data/processed/`. Do not trust the `city` text field alone.
153 +
154 +### 4.3 Cleaning
155 +Reproducible pipeline; raw CSV untouched. Use `log(amount)`. Investigate zeros, implausible prices, non-arm's-length transfers, duplicates (flag with `duplicate_flag`/`duplicate_reason`, never silently delete). Produce a full exclusion table. Economically justified filters only — no unreported winsorizing.
156 +
157 +### 4.4 Hedonic model
158 +Pool 2021–present. Do NOT run independent weekly regressions.
159 +
160 +- **Model A (headline):** structural characteristics only — type, log(floorArea), age (spline/bins, data-driven vintage breakpoints), buildingType, fine location controls.
161 +- **Model B (robustness):** adds `totalArValue`, `previousValue` — beware valuation leakage; never the headline.
162 +
163 +Missingness: compare missing-indicator, conditional-median imputation, MICE, ML imputation. No future-price leakage. Choose stability + interpretability.
164 +
165 +### 4.5 Location control — critical
166 +Compare municipality FE, postal-code FE, H3/geohash grid, 2D splines on lat/lng, hierarchical spatial effects. Westmount ≠ rest of Montreal. Diagnose residual spatial autocorrelation; improve until it is acceptable.
167 +
168 +### 4.6 Hierarchical weekly state
169 +Latent path per `geography × property_type`:
170 +
171 +```
172 +Quebec trend + region deviation + municipality deviation + type deviation + local week deviation
173 +```
174 +
175 +Thin cells shrink toward parent trends; liquid cells (Montreal condo ~145/wk, Quebec City condo ~42/wk) are dominated by local data. Evaluate: Bayesian multilevel, empirical Bayes, mixed effects, state-space/Kalman (`μ_t = μ_{t-1} + η_t`), penalized splines. Benchmark stability and compute cost; do not choose complexity for its own sake. Also benchmark a two-stage architecture (structural hedonic residualization → hierarchical weekly time-series) against a unified time-dummy regression.
176 +
177 +### 4.7 Validation (mandatory)
178 +- Repeat-sales index (~85k repeated addresses; beware condo/plex address sharing) as directional check.
179 +- Downsampling experiment: use Montreal condos as lab; thin to 100/50/25/15/10/5 tx/week; measure RMSE, bias, volatility, turning-point accuracy, CI coverage. This empirically justifies liquidity tiers (starting hypotheses: ≥50 very strong, 20–49 strong, 10–19 shrinkage, 5–9 heavy shrinkage, <5 model-implied).
180 +- Composition-shock simulation: raw median must move, hedonic index must not.
181 +- Temporal + geographic holdouts; alternative specs (GAM, mixed effects, hierarchical Bayes, gradient-boosting residualization). ML (LightGBM/CatBoost/XGBoost) may estimate the cross-sectional component only — the time effect stays interpretable.
182 +- Weekly vs monthly comparison from the same methodology: quantify signal-to-noise, turning-point detection, revision magnitude.
183 +
184 +### 4.8 Outputs per series
185 +Base: 2021 average = 100 (keep raw latent log series). Both `index` (raw weekly) and `index_smoothed` (one-sided real-time version required alongside any two-sided smoother). Representative dollar value per segment from a documented property basket. Growth: 1w, 4w, 13w, 26w, YoY, YTD, since-2021. 95% CIs (cluster/block bootstrap or posterior). Reliability grades A–E from n, effective N, SE, shrinkage weight, missingness. NSA always; SA only if stable seasonality is demonstrated. `is_partial_week` nowcast handling for the latest week. Vintage framework: `first_release`, `current_vintage`, `revision`.
186 +
187 +### 4.9 Canonical dataset
188 +
189 +`data/processed/qwhpi_weekly.parquet`:
190 +
191 +```
192 +week, geography_level, geography_id, geography_name, property_type,
193 +index, index_smoothed, representative_value,
194 +transactions, effective_sample_size,
195 +weekly_pct, four_week_pct, thirteen_week_pct, yoy_pct,
196 +lower_95, upper_95, reliability_grade, shrinkage_weight,
197 +is_partial_week, model_version, data_vintage
198 +```
199 +
200 +This single table powers the database, API, and dashboard.
201 +
202 +### 4.10 Index families
203 +- **QWHPI-QC** (province), **QWHPI-REG** (17 regions), **QWHPI-CITY** (Montréal, Québec, Laval, Gatineau, Longueuil, Sherbrooke, Trois-Rivières, Saguenay, Lévis, Drummondville — as liquidity permits), **QWHPI-TYPE** (All / Single-family / Condo / Plex per supported geography).
204 +- Secondary module: **Assessment Gap Index** from `amount / totalArValue` — separate concept, never mixed with the price index.
205 +- Coverage matrix declaring, per municipality × type: published / conditional / not published.
206 +
207 +---
208 +
209 +## 5. Data Store
210 +
211 +- Parquet lake in `data/processed/` (partitioned by geography_level / year) is the source of truth.
212 +- PostgreSQL (via docker-compose) serves the API: tables `series`, `observations`, `geographies`, `liquidity`, `vintages`, `model_runs`. Alembic migrations in `db/migrations/`.
213 +- Loader `engine/src/qwhpi/export.py` upserts each pipeline run into the DB with `model_version` and `data_vintage`.
214 +- Never let the API read raw CSVs.
215 +
216 +---
217 +
218 +## 6. API (FastAPI)
219 +
220 +Endpoints:
221 +
222 +```
223 +GET /v1/index?geography=quebec-city&type=condo&from=2021-01-04&to=latest
224 +GET /v1/index/latest?geography=...&type=...
225 +GET /v1/geographies # hierarchy + coverage matrix
226 +GET /v1/liquidity?geography=...&type=...
227 +GET /v1/compare?series=montreal:condo,quebec-city:condo
228 +GET /v1/map?metric=yoy&level=region
229 +GET /v1/vintages?geography=...&type=...&week=...
230 +GET /v1/meta # model_version, data_vintage, methodology link, author credit
231 +GET /health
232 +```
233 +
234 +Sample response:
235 +
236 +```json
237 +{
238 + "geography": "Quebec City",
239 + "property_type": "condo",
240 + "frequency": "weekly",
241 + "latest_index": 137.42,
242 + "representative_value": 389200,
243 + "weekly_change": 0.31,
244 + "yoy_change": 6.74,
245 + "transactions": 47,
246 + "reliability": "A",
247 + "lower_95": 135.9,
248 + "upper_95": 138.9,
249 + "is_partial_week": false,
250 + "author": "Simon-Pierre Boucher",
251 + "contact": "contact@spboucher.ai"
252 +}
253 +```
254 +
255 +Requirements: Pydantic schemas, OpenAPI docs at `/docs`, pagination for full histories, CSV/JSON export toggle, ETag caching, rate limiting, CORS for the web app, tests for every router. Reliability and CIs are always returned — the API never hides uncertainty.
256 +
257 +---
258 +
259 +## 7. Dashboard (Next.js + TypeScript)
260 +
261 +Pages:
262 +- **/** — headline: Quebec aggregate, latest week, YoY, sparkline, top movers.
263 +- **/explore** — series picker (geography tree × property type), weekly chart with CI band, raw-median overlay toggle, smoothed toggle, growth-horizon selector, transaction-volume subchart, reliability badge, CSV download.
264 +- **/compare** — multi-series comparison (e.g., condo across major cities), rebasing tool.
265 +- **/map** — choropleth of the 17 regions (YoY, 13w, index level, assessment gap); H3 heat map for major urban areas where density allows.
266 +- **/methodology** — rendered methodology summary; link to the paper.
267 +- **/api-docs** — link/embed of OpenAPI docs.
268 +
269 +Requirements: charting via a solid library (e.g., ECharts/Recharts/Plotly), responsive, dark/light, consistent color config shared with engine figures, loading/empty/error states, low-reliability series visually flagged (never hidden), footer credit "Simon-Pierre Boucher — contact@spboucher.ai".
270 +
271 +---
272 +
273 +## 8. Automation & Ops
274 +
275 +- `make pipeline` → full engine run; `make refresh` → incremental update when new transactions are appended; `make api` / `make web` / `make up` (docker-compose).
276 +- Scheduler (`ops/scheduler/`) runs the weekly refresh: ingest new rows → append-only clean → re-estimate weekly states → write new `data_vintage` → load DB → invalidate API cache.
277 +- CI: lint, type-check (mypy + tsc), tests, **header check**, small-sample smoke run of the pipeline.
278 +- Structured logging + run manifest (input hash, row counts, model version, timings) for every pipeline execution.
279 +
280 +---
281 +
282 +## 9. Engineering Standards
283 +
284 +- Python: pandas/polars, numpy, statsmodels, scikit-learn, geopandas, pyarrow; PyMC/CmdStanPy or statsmodels state-space where justified. Vectorized; no per-transaction Python loops; cache spatial joins, features, model matrices; Parquet everywhere.
285 +- Diagnostics for every production model: residual distribution, heteroskedasticity, residuals-vs-fitted, temporal/spatial residual patterns, coefficient stability, weekly SEs, effective N.
286 +- Model selection optimizes interpretability + stability + low bias + responsiveness + calibrated uncertainty + reproducibility — NOT transaction-level RMSE alone. This is economic measurement, not a prediction contest.
287 +- Core principle everywhere: separate **price movement** from **composition movement**. Never present a weekly average price as a price level. Never fake precision — expose n, effective N, CI, shrinkage weight, reliability.
288 +
289 +---
290 +
291 +## 10. Research Outputs
292 +
293 +- `outputs/figures/`: Quebec aggregate; region comparison; big-4 city comparison; condo/single-family/plex by city; raw median vs hedonic; volumes; CI bands; reliability map; repeat-sales vs hedonic; downsampling stability; assessment-gap evolution; YoY appreciation map. One shared plotting config.
294 +- `outputs/tables/`: data_summary, missingness, exclusions, weekly_liquidity, coverage, model_comparison, index_latest, repeat_sales_comparison, downsampling_results, revision_statistics.
295 +- `paper/`: full LaTeX paper — *A High-Frequency Hedonic Housing Price Index for Quebec* — real, researched citations only (hedonic indexes, repeat sales, Case-Shiller, high-frequency measurement, hierarchical indexes, state-space indexes, spatial hedonics, index-number theory). No fabricated references.
296 +- Research questions to actually investigate: regional heterogeneity post-2021; synchronization of turning points; Montreal lead/lag; type-level response speeds; weekly-vs-monthly information gain; minimum transactions for reliability; value of hierarchical pooling; assessment lag dynamics.
297 +
298 +---
299 +
300 +## 11. Execution Order
301 +
302 +1. **Audit** — schema, descriptives, missingness, outliers, coverage, weekly liquidity matrices. No index yet.
303 +2. **Geography** — boundary acquisition, spatial join, validation, persistence.
304 +3. **Cleaning** — research sample with documented exclusions.
305 +4. **Baseline index** — transparent pooled hedonic time-dummy for Quebec + Montreal/Québec/Laval/Gatineau × All/Unifamilial/Condo/Plex.
306 +5. **Hierarchical weekly model** — shrinkage for thin segments.
307 +6. **Validation** — repeat sales, downsampling, monthly comparison, alternative specs.
308 +7. **Scale** — all supported region and municipality series; coverage matrix.
309 +8. **Data store + API** — DB schema, loader, FastAPI, tests.
310 +9. **Dashboard** — all pages against the live API.
311 +10. **Automation + research outputs** — scheduler, CI, figures, tables, paper.
312 +
313 +Work autonomously: inspect data → research accepted methodology → implement the most defensible option → document → benchmark alternatives → preserve reversibility. Do not settle for the first model that runs. Do not stop to ask permission on minor modeling choices.
314 +
315 +---
316 +
317 +## 12. Definition of Success
318 +
319 +One reproducible command (`make up` after `make pipeline`) yields:
320 +
321 +- a trustworthy weekly, quality-adjusted, hierarchically pooled index for every statistically defensible `geography × property_type`, 2021 → latest week, with CIs, reliability grades, representative dollar values, validation, and vintages;
322 +- a documented API serving it;
323 +- an interactive dashboard displaying it;
324 +- publication-quality figures, tables, and a methodology paper;
325 +- **every file in the repository headed with: Simon-Pierre Boucher — contact@spboucher.ai.**
added Makefile +63 −0
@@ -0,0 +1,63 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : Makefile
6 +# Purpose : One-command targets — pipeline, refresh, api, web, docker, tests.
7 +# =============================================================================
8 +
9 +PY := python3
10 +ENGINE := engine/scripts
11 +
12 +.PHONY: pipeline refresh api web up down test lint headers figures paper clean
13 +
14 +## Full engine run: audit -> geography -> clean -> baseline -> hierarchical
15 +## -> validation -> canonical dataset (+ DB load when DATABASE_URL is set)
16 +pipeline:
17 + $(PY) $(ENGINE)/01_profile.py
18 + $(PY) $(ENGINE)/02_geography.py
19 + $(PY) $(ENGINE)/03_clean.py
20 + $(PY) $(ENGINE)/04_baseline.py
21 + $(PY) $(ENGINE)/05_hierarchical.py
22 + $(PY) $(ENGINE)/06_validation.py
23 + $(PY) $(ENGINE)/07_canonical.py
24 + $(PY) $(ENGINE)/09_monthly.py
25 + $(PY) $(ENGINE)/10_monthly_validation.py
26 + $(PY) $(ENGINE)/11_monthly_canonical.py
27 + $(PY) -c "import sys; sys.path.insert(0,'engine/src'); from qwhpi.export import load_database; load_database()"
28 +
29 +## Incremental refresh (new rows appended to the raw CSV) — monthly headline
30 +refresh:
31 + $(PY) $(ENGINE)/03_clean.py
32 + $(PY) $(ENGINE)/09_monthly.py
33 + $(PY) $(ENGINE)/11_monthly_canonical.py
34 + $(PY) -c "import sys; sys.path.insert(0,'engine/src'); from qwhpi.export import load_database; load_database()"
35 +
36 +api:
37 + cd api && $(PY) -m uvicorn app.main:app --host 0.0.0.0 --port 8080
38 +
39 +web:
40 + cd web && npm run build && npm run start
41 +
42 +up:
43 + docker compose up --build -d
44 +
45 +down:
46 + docker compose down
47 +
48 +test:
49 + cd api && $(PY) -m pytest tests -q
50 + cd engine && $(PY) -m pytest tests -q
51 +
52 +headers:
53 + $(PY) scripts/check_headers.py
54 +
55 +figures:
56 + $(PY) $(ENGINE)/08_figures.py
57 +
58 +paper:
59 + cd paper && latexmk -pdf -quiet qwhpi.tex || pdflatex qwhpi.tex
60 +
61 +lint:
62 + $(PY) scripts/check_headers.py
63 + cd web && npx tsc --noEmit
added README.md +501 −0
@@ -0,0 +1,501 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : README.md
7 +Purpose : Full platform documentation — methodology, architecture, API,
8 + dashboard, data policy, reproducibility.
9 +=============================================================================
10 +-->
11 +
12 +<div align="center">
13 +
14 +# 🏠 QHPI — Quebec Housing Price Index
15 +
16 +### Quality-adjusted, hierarchically pooled housing price indexes for Quebec
17 +
18 +*Province · 17 administrative regions · major municipalities × {all, unifamilial, condo, plex}*
19 +
20 +[![Live](https://img.shields.io/badge/live-www.indexqc.house-2ea44f?style=flat-square&logo=googlechrome&logoColor=white)](https://www.indexqc.house)
21 +[![Model](https://img.shields.io/badge/model-v2.1.0-blue?style=flat-square)](#-methodology)
22 +[![Frequency](https://img.shields.io/badge/frequency-monthly%20(headline)%20%2B%20weekly%20(research)-8a2be2?style=flat-square)](#-methodology)
23 +[![Coverage](https://img.shields.io/badge/coverage-2021--01%20%E2%86%92%20present-informational?style=flat-square)](#-index-families--coverage)
24 +[![Series](https://img.shields.io/badge/published%20cells-81%20(50%20direct%20%2B%2031%20hierarchical)-orange?style=flat-square)](#-methodology)
25 +[![Transactions](https://img.shields.io/badge/estimation%20sample-~720k%20transactions-lightgrey?style=flat-square)](#-data-policy--no-microdata-in-this-repo)
26 +
27 +[![Python](https://img.shields.io/badge/Python-3.11+-3776AB?style=flat-square&logo=python&logoColor=white)](engine/pyproject.toml)
28 +[![FastAPI](https://img.shields.io/badge/API-FastAPI-009688?style=flat-square&logo=fastapi&logoColor=white)](api/)
29 +[![Next.js](https://img.shields.io/badge/Web-Next.js%20%2B%20TypeScript-000000?style=flat-square&logo=nextdotjs&logoColor=white)](web/)
30 +[![PostgreSQL](https://img.shields.io/badge/DB-PostgreSQL-4169E1?style=flat-square&logo=postgresql&logoColor=white)](db/)
31 +[![Docker](https://img.shields.io/badge/Ops-docker--compose-2496ED?style=flat-square&logo=docker&logoColor=white)](docker-compose.yml)
32 +[![LaTeX](https://img.shields.io/badge/Paper-LaTeX-008080?style=flat-square&logo=latex&logoColor=white)](paper/)
33 +
34 +[![Author](https://img.shields.io/badge/author-Simon--Pierre%20Boucher-111827?style=flat-square)](mailto:contact@spboucher.ai)
35 +[![Contact](https://img.shields.io/badge/contact-contact%40spboucher.ai-D14836?style=flat-square&logo=maildotru&logoColor=white)](mailto:contact@spboucher.ai)
36 +
37 +**Author : Simon-Pierre Boucher — [contact@spboucher.ai](mailto:contact@spboucher.ai)**
38 +
39 +</div>
40 +
41 +---
42 +
43 +## Table of contents
44 +
45 +- [What this is](#-what-this-is)
46 +- [Key numbers](#-key-numbers)
47 +- [Data policy — no microdata in this repo](#-data-policy--no-microdata-in-this-repo)
48 +- [Platform architecture](#-platform-architecture)
49 +- [Methodology](#-methodology)
50 +- [Validation](#-validation)
51 +- [Index families & coverage](#-index-families--coverage)
52 +- [Canonical dataset](#-canonical-dataset)
53 +- [API](#-api)
54 +- [Dashboard](#-dashboard)
55 +- [Repository map](#-repository-map)
56 +- [Quickstart](#-quickstart)
57 +- [Engine pipeline](#-engine-pipeline)
58 +- [Engineering standards](#-engineering-standards)
59 +- [Research outputs & paper](#-research-outputs--paper)
60 +- [Reproducibility](#-reproducibility)
61 +- [Author & license](#-author--license)
62 +
63 +---
64 +
65 +## 🎯 What this is
66 +
67 +QHPI is a **production-grade economic-measurement platform**, not a median-price
68 +tracker. It separates **price movement** from **composition movement**: a week
69 +where only mansions sell must not register as a price increase.
70 +
71 +The platform has four layers:
72 +
73 +| Layer | What it does | Where |
74 +|---|---|---|
75 +| **Index Engine** | Reproducible econometric pipeline: cleaning → hedonic estimation → hierarchical pooling → validation → canonical Parquet | [`engine/`](engine/) |
76 +| **Data Store** | Versioned Parquet lake (aggregated series only) + PostgreSQL serving layer | [`data/processed/`](data/processed/), [`db/`](db/) |
77 +| **API** | FastAPI service exposing every published series with uncertainty, reliability and vintages | [`api/`](api/) |
78 +| **Dashboard** | Interactive Next.js frontend — explore, compare, map, PDF reports, live at [www.indexqc.house](https://www.indexqc.house) | [`web/`](web/) |
79 +
80 +Every published observation looks like this:
81 +
82 +```
83 +Geography: Quebec City | Type: Condo | Period: 2026-06
84 +Index: 163.5 (2021 = 100) | 1m: +0.4% | YoY: +8.1%
85 +Representative value: $389,200 | Transactions: 47 | Reliability: A
86 +95% CI: [160.9, 166.1] | Vintage: 2026-08-08 | Model: 2.1.0
87 +```
88 +
89 +Uncertainty is **never hidden**: every row carries a 95% confidence interval,
90 +an A–E reliability grade, effective sample size and shrinkage weight.
91 +
92 +---
93 +
94 +## 📊 Key numbers
95 +
96 +![Transactions](https://img.shields.io/badge/raw%20transactions-~745k-blue?style=for-the-badge)
97 +![Period](https://img.shields.io/badge/period-2021→2026-blueviolet?style=for-the-badge)
98 +![Cities](https://img.shields.io/badge/municipalities-1100%2B-orange?style=for-the-badge)
99 +![Regions](https://img.shields.io/badge/regions-17-green?style=for-the-badge)
100 +
101 +| Metric | Value |
102 +|---|---|
103 +| Raw transactions ingested | ~745,000 (2021-01 → present) |
104 +| Estimation sample after documented exclusions | ~720,000 |
105 +| Geographic units | Province + 17 administrative regions + major municipalities |
106 +| Property types | `unifamilial`, `condo`, `plex` (+ `all` composite) |
107 +| Published cells | 81 — 50 **direct** liquid cells + 31 **hierarchical** thin cells |
108 +| Headline frequency | **Monthly** (v2.1) — weekly v1 retained as a research module |
109 +| Base | 2021 average = 100 |
110 +| Reliability grades | **A** ≥150 tx/mo · **B** ≥75 · **C** ≥40 · **D** ≥15 · **E** <15 (model-implied) |
111 +| Growth horizons | 1m, 3m, 6m, YoY (weekly module: 1w, 4w, 13w, 26w, YoY) |
112 +| Vintages | `first_release` preserved; every revision queryable |
113 +
114 +---
115 +
116 +## 🔒 Data policy — no microdata in this repo
117 +
118 +> **This repository contains NO individual transaction data.**
119 +
120 +| Category | In repo? | Detail |
121 +|---|---|---|
122 +| Raw transaction CSV (`data/raw/`) | ❌ **excluded** | Individual sale records — never versioned |
123 +| Transaction-level intermediates (`data/interim/`, `transactions_clean.parquet`, `geo_join.parquet`) | ❌ **excluded** | Anything with one row per transaction |
124 +| Cadastral boundary files (`SDA.gpkg`) | ❌ excluded (size) | Source, URL, version and CRS documented in [`data/external/SDA_SOURCE.md`](data/external/SDA_SOURCE.md) |
125 +| **Aggregated published series** | ✅ included | `qhpi_monthly.parquet`, `qwhpi_weekly.parquet`, liquidity, coverage matrices, assessment gap, first releases, vintages |
126 +| **Aggregate tables & figures** (`outputs/`) | ✅ included | Audit counts, model comparisons, validation results, publication figures |
127 +
128 +The [`.gitignore`](.gitignore) enforces this at the repo boundary. Aggregates
129 +are cell-level (`geography × type × period`) with transaction **counts**, never
130 +transaction rows. To rerun the full pipeline you must supply your own
131 +`data/raw/province_transactions.csv` (see [Reproducibility](#-reproducibility)).
132 +
133 +---
134 +
135 +## 🏗 Platform architecture
136 +
137 +```
138 + ┌─────────────────────────────────────────────┐
139 + │ province_transactions.csv │
140 + │ (local only — NOT in this repo) │
141 + └──────────────────────┬──────────────────────┘
142 +
143 + ┌───────────────────────────── ENGINE (Python) ─────────────────────────────┐
144 + │ 01 profile → 02 geography (spatial join, SDA boundaries) → 03 clean │
145 + │ → 04–08 weekly research module (baseline, hierarchical, validation) │
146 + │ → 09 monthly estimation (rolling time-dummy, Huber-IRLS) │
147 + │ → 10 monthly validation → 11 canonical parquet │
148 + └──────────────────────────────────┬────────────────────────────────────────┘
149 +
150 + data/processed/qhpi_monthly.parquet ← source of truth
151 + (+ liquidity, coverage, vintages, assessment gap)
152 +
153 + ┌────────────── PostgreSQL (docker-compose, Alembic) ──────────────┐
154 + │ series · observations · geographies · liquidity · vintages │
155 + └──────────────────────────────┬────────────────────────────────────┘
156 +
157 + FastAPI /v1/* (uncertainty always returned)
158 +
159 + Next.js dashboard — www.indexqc.house
160 + (explore · compare · map · PDF reports · methodology)
161 +```
162 +
163 +Weekly refresh: `ops/scheduler/` ingests new rows → append-only clean →
164 +re-estimation → new `data_vintage` → DB load → API cache invalidation.
165 +
166 +---
167 +
168 +## 🧪 Methodology
169 +
170 +The index is a **robust hedonic time-dummy index with hierarchical pooling** —
171 +three estimation regimes arbitrated by empirical validation (repeat sales +
172 +stratified matched-cell medians), not by convenience:
173 +
174 +### 1. Province × type backbone — rolling time-dummy (RTD)
175 +
176 +- Pooled robust regressions (`log(price)` on structural characteristics +
177 + fine location fixed effects + period dummies) over **13-month rolling
178 + windows** with **Huber-IRLS** weighting.
179 +- Windows are combined by **mean splice**, making published history
180 + **revision-free** by construction.
181 +- Coefficient drift is monitored and published
182 + (`outputs/tables/rtd_coefficient_drift.csv`) — e.g. the floor-area
183 + elasticity moves 0.556 → 0.586 across windows, which a fixed pooled model
184 + would have hidden.
185 +
186 +### 2. Liquid cells (median ≥ 40 tx/month) — direct local estimation
187 +
188 +Each of the **50 liquid `geography × type` cells** gets its **own robust
189 +time-dummy regression** (own hedonic coefficients, own FSA fixed effects),
190 +lightly smoothed by a state-space filter. This was arbitrated against
191 +deviation-from-pooled-surface modelling, which **compressed genuine local
192 +divergence** — Quebec City condos moved ~+55% (confirmed independently by
193 +repeat sales *and* stratified matched-cell medians) while pooled deviations
194 +reported only +42%.
195 +
196 +### 3. Thin cells — hierarchical Kalman shrinkage
197 +
198 +The remaining **31 thin cells** follow a latent random-walk **deviation from
199 +their parent's published path**, estimated by Kalman filter/smoother with
200 +observation noise σ²/nₜ. Thin months shrink toward the parent trend; the
201 +shrinkage weight is published on every row.
202 +
203 +### Hedonic specification (headline = structural only)
204 +
205 +- `log(amount)` on `log(floorArea)` (+ time-stable missingness indicator),
206 + age from `yearBuilt` via **conditional-median imputation** (its missingness
207 + drifts 15% → 2% over the sample, so a "missing" bin would leak time),
208 + and fine location fixed effects (FSA).
209 +- **`buildingType` is banned from all hedonic models.** The provider
210 + backfilled it in region-staggered waves (most regions 2022-01, Montreal
211 + 2023-01); the time-correlated missingness fabricated a −38 log-point cliff
212 + in local Montreal indexes. The ban is documented in
213 + [`engine/src/qwhpi/features.py`](engine/src/qwhpi/features.py).
214 +- Assessment values (`totalArValue`, `previousValue`) never enter the
215 + headline (valuation leakage); they power the separate
216 + **Assessment Gap Index** (`amount / totalArValue`).
217 +
218 +### Cleaning
219 +
220 +Raw data is immutable; every exclusion is economically justified and counted
221 +in [`outputs/tables/exclusions.csv`](outputs/tables/exclusions.csv)
222 +(assessment-ratio band, exact duplicates, non-arm's-length transfers, …).
223 +Duplicates are **flagged, never silently deleted**. The `indéterminé`
224 +property type never contaminates type-specific indexes.
225 +
226 +### Geography
227 +
228 +Transactions are spatially joined (`lat/lng`) against the authoritative
229 +Quebec **SDA cadastral boundaries** (source, version and CRS documented in
230 +[`data/external/SDA_SOURCE.md`](data/external/SDA_SOURCE.md)) — the free-text
231 +`city` field alone is not trusted.
232 +
233 +---
234 +
235 +## ✅ Validation
236 +
237 +Every claim below has a table or figure in [`outputs/`](outputs/):
238 +
239 +| Check | Result |
240 +|---|---|
241 +| **Repeat-sales benchmark** (BMN, ~85k repeated addresses) | June 2026, 2021=100: quebec/condo **132.0** vs RS 132 (exact); quebec-city/condo **163.5** vs RS 162; montreal/condo **121.6** vs RS 122; montreal/unifamilial **126.2** between stratified 120 and RS 135 (RS renovation bias on older stock) |
242 +| **Composition-shock simulation** | Mix shock moves the raw median **+11.7%**; the hedonic index moves **−0.7%** — the index measures prices, not composition |
243 +| **Downsampling experiment** | Montreal condos thinned to 100/50/25/15/10/5 tx/period; RMSE, bias, volatility, turning points and CI coverage justify the A–E liquidity tiers empirically |
244 +| **Weekly vs monthly** | Same methodology at both frequencies; signal-to-noise, turning-point detection and revision magnitude quantified in `outputs/tables/weekly_vs_monthly.csv` |
245 +| **Seasonality** | NSA published; no stable seasonality demonstrated (`seasonality_tests.csv`) |
246 +| **Alternative specs** | Hierarchical variants, mixed effects, ML residualization benchmarked in `estimation_methods.csv` — chosen model optimizes interpretability + stability, not transaction-level RMSE |
247 +
248 +Full write-ups: [`outputs/reports/`](outputs/reports/) (audit, geography,
249 +cleaning, hierarchical, validation v1 & v2, methodology research).
250 +
251 +---
252 +
253 +## 🗺 Index families & coverage
254 +
255 +| Family | Scope |
256 +|---|---|
257 +| **QHPI-QC** | Province of Quebec |
258 +| **QHPI-REG** | 17 administrative regions |
259 +| **QHPI-CITY** | Montréal, Québec, Laval, Gatineau, Longueuil, Sherbrooke, Trois-Rivières, Saguenay, Lévis, Drummondville, … as liquidity permits |
260 +| **QHPI-TYPE** | All / Unifamilial / Condo / Plex per supported geography |
261 +| **Assessment Gap** | `amount / totalArValue` — separate concept, never mixed with the price index |
262 +
263 +The **coverage matrix**
264 +([`data/processed/coverage_matrix_monthly.parquet`](data/processed/),
265 +[`outputs/tables/coverage_monthly.csv`](outputs/tables/coverage_monthly.csv))
266 +declares per `municipality × type`: published / conditional / not published.
267 +
268 +---
269 +
270 +## 🗃 Canonical dataset
271 +
272 +[`data/processed/qhpi_monthly.parquet`](data/processed/qhpi_monthly.parquet)
273 +(7,504 rows) powers the DB, API and dashboard. Weekly research module:
274 +[`data/processed/qwhpi_weekly.parquet`](data/processed/qwhpi_weekly.parquet)
275 +(32,592 rows).
276 +
277 +```
278 +period, geography_level, geography_id, geography_name, property_type,
279 +index, index_smoothed, representative_value,
280 +transactions, effective_sample_size,
281 +monthly_pct, three_month_pct, six_month_pct, yoy_pct,
282 +lower_95, upper_95, reliability_grade, shrinkage_weight,
283 +is_partial_month, model_version, data_vintage
284 +```
285 +
286 +- `index` = raw estimate, `index_smoothed` = **one-sided** (real-time safe)
287 + smoother — no look-ahead in the published real-time series.
288 +- `representative_value` = dollar value of a documented, fixed property
289 + basket per segment.
290 +- Vintage framework: first releases preserved under
291 + `data/processed/vintages*/`, revisions queryable via the API.
292 +
293 +---
294 +
295 +## 🔌 API
296 +
297 +FastAPI service (OpenAPI docs at `/docs`), CSV/JSON export toggle, ETag
298 +caching, pagination, rate limiting, CORS. **Reliability and CIs are always
299 +returned.**
300 +
301 +| Endpoint | Purpose |
302 +|---|---|
303 +| `GET /v1/index?geography=quebec-city&type=condo&from=2021-01&to=latest` | Full series |
304 +| `GET /v1/index/latest?geography=…&type=…` | Latest observation |
305 +| `GET /v1/geographies` · `GET /v1/geographies/coverage` | Hierarchy + coverage matrix |
306 +| `GET /v1/liquidity?geography=…&type=…` | Transaction counts & effective N |
307 +| `GET /v1/compare?series=montreal:condo,quebec-city:condo` | Multi-series comparison |
308 +| `GET /v1/map?metric=yoy&level=region` | Choropleth payloads (+ `period` for time-lapse) |
309 +| `GET /v1/stats` · `GET /v1/stats/overview` | Peak/drawdown/CAGR/volatility/momentum/rank, heatmap rows |
310 +| `GET /v1/report?series=…` · `GET /v1/report/market` | Publication-grade PDF reports (matplotlib) |
311 +| `GET /v1/vintages?geography=…&type=…` | Revision history |
312 +| `GET /v1/meta` | `model_version`, `data_vintage`, methodology link, author credit |
313 +| `GET /v1/health` | Liveness |
314 +
315 +Sample response:
316 +
317 +```json
318 +{
319 + "geography": "Quebec City",
320 + "property_type": "condo",
321 + "frequency": "monthly",
322 + "latest_index": 163.5,
323 + "representative_value": 389200,
324 + "monthly_change": 0.4,
325 + "yoy_change": 8.1,
326 + "transactions": 47,
327 + "reliability": "A",
328 + "lower_95": 160.9,
329 + "upper_95": 166.1,
330 + "is_partial_month": false,
331 + "author": "Simon-Pierre Boucher",
332 + "contact": "contact@spboucher.ai"
333 +}
334 +```
335 +
336 +---
337 +
338 +## 💻 Dashboard
339 +
340 +Live at **[www.indexqc.house](https://www.indexqc.house)** — Next.js +
341 +TypeScript, dark/light themes, fully responsive (audited at 390/360 px),
342 +`prefers-reduced-motion` respected, zero-dependency custom hero chart,
343 +⌘K command palette over all 112 series.
344 +
345 +| Page | Contents |
346 +|---|---|
347 +| `/` | Market pulse: heat-shaded region table with sparklines, 12 metric tiles, ticker tape, top movers, market-report PDF |
348 +| `/explore` | Series picker, CI band, raw-median & smoothed toggles, 1Y/3Y/5Y/YTD/MAX ranges, Bank-of-Canada event annotations, brush zoom, PNG/CSV export, sortable data table, shareable URLs |
349 +| `/compare` | Multi-series with rebasing tool, toggleable legend chips, comparison PDF |
350 +| `/map` | Region choropleth (YoY, 3m, level, assessment gap) with monthly slider + ▶ time-lapse |
351 +| `/methodology` | Rendered methodology + link to the paper |
352 +| `/api-docs` | Live playground (run requests in-page) + copyable curl examples |
353 +
354 +Low-reliability series are **visually flagged, never hidden**. Footer credit:
355 +*Simon-Pierre Boucher — contact@spboucher.ai*.
356 +
357 +---
358 +
359 +## 📁 Repository map
360 +
361 +```
362 +├── engine/ # Index engine (Python)
363 +│ ├── src/qwhpi/ # ingest · clean · geography · features · hedonic
364 +│ │ # rtd · hierarchy · state_space · repeat_sales
365 +│ │ # index · uncertainty · seasonal · nowcast
366 +│ │ # vintages · export · plotting
367 +│ ├── scripts/ # 01_profile … 11_monthly_canonical (ordered, idempotent)
368 +│ └── tests/ # smoke pipeline, features/clean, state-space
369 +├── data/
370 +│ ├── external/ # boundary-source documentation (files excluded by size)
371 +│ └── processed/ # ✅ aggregated series only (see Data policy)
372 +├── db/ # PostgreSQL schema + Alembic migrations
373 +├── api/ # FastAPI app: routers/ services/ tests/
374 +├── web/ # Next.js dashboard: app/ components/ lib/ styles/
375 +├── ops/ # weekly-refresh scheduler + CI pipeline
376 +├── outputs/
377 +│ ├── figures/ # publication figures (shared plotting config)
378 +│ ├── tables/ # 35+ audit/validation/model tables (aggregates)
379 +│ └── reports/ # markdown reports for every pipeline stage
380 +├── paper/ # LaTeX methodology paper (+ compiled PDF)
381 +├── scripts/check_headers.py # CI gate: every file carries the author header
382 +├── Makefile # one-command targets
383 +└── docker-compose.yml # postgres + api + web + scheduler
384 +```
385 +
386 +---
387 +
388 +## 🚀 Quickstart
389 +
390 +> Requires the raw transaction CSV (not distributed — see
391 +> [Data policy](#-data-policy--no-microdata-in-this-repo)) for `make pipeline`.
392 +> The API and dashboard run off the **included aggregated Parquet** without it.
393 +
394 +```bash
395 +# Engine (full run ~ audit → geography → clean → estimate → validate → canonical)
396 +make pipeline
397 +
398 +# Incremental monthly refresh (clean → estimate → canonical, ~2 min)
399 +make refresh
400 +
401 +# Services
402 +make api # FastAPI on :8080 — docs at /docs
403 +make web # Next.js dashboard on :3000
404 +make up # docker compose: postgres + api + web + scheduler
405 +make down
406 +
407 +# Quality
408 +make test # engine + api test suites
409 +make lint # ruff + mypy + tsc
410 +make headers # author-header check (also a pre-commit hook + CI gate)
411 +make figures # publication figures → outputs/figures/
412 +make paper # compile the LaTeX paper
413 +```
414 +
415 +---
416 +
417 +## ⚙️ Engine pipeline
418 +
419 +| Script | Stage |
420 +|---|---|
421 +| `01_profile.py` | Data audit: schema, descriptives, missingness, sentinel values, liquidity matrices |
422 +| `02_geography.py` | SDA boundary spatial join, validation, persistence |
423 +| `03_clean.py` | Research sample with fully documented exclusions |
424 +| `04_baseline.py` | Weekly pooled hedonic time-dummy baseline *(research module)* |
425 +| `05_hierarchical.py` | Weekly hierarchical state-space shrinkage *(research module)* |
426 +| `06_validation.py` | Repeat sales, downsampling, composition shock *(weekly)* |
427 +| `07_canonical.py` | Weekly canonical Parquet *(research module)* |
428 +| `08_figures.py` | Publication figures |
429 +| `09_monthly.py` | **Headline monthly estimation** (RTD + direct cells + Kalman) |
430 +| `10_monthly_validation.py` | Monthly validation suite |
431 +| `11_monthly_canonical.py` | **Canonical `qhpi_monthly.parquet`** + vintage write |
432 +
433 +Every run emits a structured **run manifest** (input hash, row counts, model
434 +version, timings).
435 +
436 +---
437 +
438 +## 📐 Engineering standards
439 +
440 +- **Every file** starts with the author header — enforced by
441 + [`scripts/check_headers.py`](scripts/check_headers.py) in pre-commit **and** CI.
442 +- Vectorized pandas/polars + numpy; no per-transaction Python loops; spatial
443 + joins, features and model matrices cached as Parquet.
444 +- Diagnostics for every production model: residual distribution,
445 + heteroskedasticity, temporal/spatial residual patterns, coefficient
446 + stability, effective N.
447 +- Model selection optimizes **interpretability + stability + calibrated
448 + uncertainty + reproducibility** — never transaction-level RMSE alone.
449 +- CI (`ops/ci/ci.yml`): lint, mypy + tsc, tests, header check, small-sample
450 + smoke run of the pipeline.
451 +
452 +---
453 +
454 +## 📄 Research outputs & paper
455 +
456 +- **Figures** ([`outputs/figures/`](outputs/figures/)): province aggregate,
457 + region comparison, big-4 cities, condo by city, raw median vs hedonic,
458 + volumes, reliability matrix, downsampling stability, assessment gap,
459 + YoY appreciation map.
460 +- **Tables** ([`outputs/tables/`](outputs/tables/)): audit suite, exclusions,
461 + liquidity, coverage, model comparison, repeat-sales comparison,
462 + downsampling, seasonality, coefficient drift, weekly-vs-monthly.
463 +- **Paper** ([`paper/qwhpi.pdf`](paper/qwhpi.pdf)): *A High-Frequency Hedonic
464 + Housing Price Index for Quebec* — real, researched citations only (hedonic
465 + and repeat-sales index theory, Case-Shiller, hierarchical and state-space
466 + indexes, spatial hedonics, index-number theory).
467 +
468 +---
469 +
470 +## 🔁 Reproducibility
471 +
472 +1. Place your transaction extract at `data/raw/province_transactions.csv`
473 + (columns: `id, date, amount, street, zipCode, city, lat, lng,
474 + propertyType, yearBuilt, floorArea, buildingType, previousValue,
475 + totalArValue, ownerType`).
476 +2. Download the SDA boundary file per
477 + [`data/external/SDA_SOURCE.md`](data/external/SDA_SOURCE.md).
478 +3. `make pipeline` — deterministic given the same inputs; every run is
479 + stamped with `model_version` + `data_vintage` and manifested.
480 +
481 +Without microdata, everything downstream of the canonical Parquet (DB load,
482 +API, dashboard, figures) is fully runnable from the aggregates included here.
483 +
484 +---
485 +
486 +## 👤 Author & license
487 +
488 +<div align="center">
489 +
490 +**Simon-Pierre Boucher**
491 +
492 +[![Email](https://img.shields.io/badge/✉️%20contact@spboucher.ai-D14836?style=for-the-badge)](mailto:contact@spboucher.ai)
493 +[![Site](https://img.shields.io/badge/🌐%20www.indexqc.house-2ea44f?style=for-the-badge)](https://www.indexqc.house)
494 +
495 +</div>
496 +
497 +Code, methodology, figures and documentation © Simon-Pierre Boucher.
498 +All rights reserved — contact the author for reuse, data questions or
499 +collaboration. The repository intentionally distributes **no individual
500 +transaction records**; published aggregates carry full uncertainty metadata
501 +so they are never mistaken for more precision than the data supports.
added api/Dockerfile +14 −0
@@ -0,0 +1,14 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/Dockerfile
6 +# Purpose : Container image for the QWHPI API service.
7 +# =============================================================================
8 +FROM python:3.12-slim
9 +WORKDIR /srv
10 +COPY pyproject.toml ./
11 +RUN pip install --no-cache-dir fastapi "uvicorn[standard]" polars pydantic "psycopg[binary]"
12 +COPY app ./app
13 +EXPOSE 8080
14 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
added api/app/__init__.py +7 −0
@@ -0,0 +1,7 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/__init__.py
6 +# Purpose : Package root for the QWHPI API application.
7 +# =============================================================================
added api/app/deps.py +44 −0
@@ -0,0 +1,44 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/deps.py
6 +# Purpose : Shared dependencies — series validation, ETag and CSV helpers.
7 +# =============================================================================
8 +"""Shared FastAPI dependencies and response helpers."""
9 +
10 +from __future__ import annotations
11 +
12 +import hashlib
13 +
14 +import polars as pl
15 +from fastapi import HTTPException, Request, Response
16 +
17 +from app.services import data
18 +
19 +VALID_TYPES = {"all", "unifamilial", "condo", "plex"}
20 +
21 +
22 +def validate_series(geography: str, property_type: str) -> None:
23 + if property_type not in VALID_TYPES:
24 + raise HTTPException(422, f"property_type must be one of {sorted(VALID_TYPES)}")
25 + if geography not in data.geography_ids():
26 + raise HTTPException(404, f"unknown geography '{geography}' — see /v1/geographies")
27 +
28 +
29 +def etag_for(request: Request, response: Response, *parts: str) -> bool:
30 + """Set the ETag; return True when the client's copy is fresh (304)."""
31 + raw = "|".join([data.data_vintage(), data.model_version(),
32 + str(request.url), *parts])
33 + tag = '"' + hashlib.sha256(raw.encode()).hexdigest()[:32] + '"'
34 + response.headers["ETag"] = tag
35 + response.headers["Cache-Control"] = "public, max-age=300"
36 + return request.headers.get("if-none-match") == tag
37 +
38 +
39 +def csv_response(df: pl.DataFrame) -> Response:
40 + return Response(
41 + content=df.write_csv(),
42 + media_type="text/csv",
43 + headers={"X-Author": "Simon-Pierre Boucher <contact@spboucher.ai>"},
44 + )
added api/app/main.py +67 −0
@@ -0,0 +1,67 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/main.py
6 +# Purpose : FastAPI application — routers, CORS, rate limiting, OpenAPI.
7 +# =============================================================================
8 +"""QWHPI API service entry point.
9 +
10 +Run locally: uvicorn app.main:app --reload --port 8080 (from api/)
11 +Docs: /docs (OpenAPI). Every response credits the author and exposes
12 +uncertainty (CIs, reliability grades) — the API never hides it.
13 +"""
14 +
15 +from __future__ import annotations
16 +
17 +import time
18 +from collections import defaultdict, deque
19 +
20 +from fastapi import FastAPI, Request
21 +from fastapi.middleware.cors import CORSMiddleware
22 +from fastapi.responses import JSONResponse
23 +
24 +from app.routers import geographies, index, liquidity, maps, meta, reports
25 +
26 +RATE_LIMIT = 120 # requests
27 +RATE_WINDOW = 60.0 # seconds
28 +_hits: dict[str, deque] = defaultdict(deque)
29 +
30 +app = FastAPI(
31 + title="QWHPI API",
32 + version="0.1.0",
33 + description=(
34 + "Quebec Weekly Housing Price Index — quality-adjusted, hierarchically "
35 + "pooled weekly indexes for Quebec geographies and property types.\n\n"
36 + "**Author: Simon-Pierre Boucher — contact@spboucher.ai**"
37 + ),
38 + contact={"name": "Simon-Pierre Boucher", "email": "contact@spboucher.ai"},
39 +)
40 +
41 +app.add_middleware(
42 + CORSMiddleware,
43 + allow_origins=["*"],
44 + allow_methods=["GET"],
45 + allow_headers=["*"],
46 +)
47 +
48 +
49 +@app.middleware("http")
50 +async def rate_limiter(request: Request, call_next):
51 + ip = request.client.host if request.client else "anon"
52 + now = time.monotonic()
53 + q = _hits[ip]
54 + while q and now - q[0] > RATE_WINDOW:
55 + q.popleft()
56 + if len(q) >= RATE_LIMIT:
57 + return JSONResponse({"detail": "rate limit exceeded"}, status_code=429)
58 + q.append(now)
59 + return await call_next(request)
60 +
61 +
62 +app.include_router(index.router, prefix="/v1", tags=["index"])
63 +app.include_router(reports.router, prefix="/v1", tags=["stats & reports"])
64 +app.include_router(geographies.router, prefix="/v1", tags=["geographies"])
65 +app.include_router(liquidity.router, prefix="/v1", tags=["liquidity"])
66 +app.include_router(maps.router, prefix="/v1", tags=["maps"])
67 +app.include_router(meta.router, tags=["meta"])
added api/app/routers/__init__.py +7 −0
@@ -0,0 +1,7 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/__init__.py
6 +# Purpose : Router package for the QWHPI API.
7 +# =============================================================================
added api/app/routers/geographies.py +63 −0
@@ -0,0 +1,63 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/geographies.py
6 +# Purpose : Geography hierarchy + coverage matrix endpoints.
7 +# =============================================================================
8 +"""Geography and coverage endpoints."""
9 +
10 +from __future__ import annotations
11 +
12 +import polars as pl
13 +from fastapi import APIRouter, Query, Request, Response
14 +
15 +from app.deps import csv_response, etag_for
16 +from app.schemas import GeographiesResponse, GeographyNode
17 +from app.services import data
18 +
19 +router = APIRouter()
20 +
21 +
22 +@router.get("/geographies", response_model=GeographiesResponse)
23 +def geographies(request: Request, response: Response):
24 + """Published series hierarchy (province → regions → municipalities)."""
25 + if etag_for(request, response):
26 + return Response(status_code=304)
27 + c = data.canonical()
28 + nodes = (
29 + c.group_by(["geography_id", "geography_name", "geography_level"])
30 + .agg(pl.col("property_type").unique().sort().alias("types"))
31 + .sort(["geography_level", "geography_id"])
32 + )
33 + return GeographiesResponse(
34 + data_vintage=data.data_vintage(),
35 + published_series=[
36 + GeographyNode(geography_id=r["geography_id"],
37 + geography_name=r["geography_name"],
38 + geography_level=r["geography_level"],
39 + property_types=r["types"])
40 + for r in nodes.iter_rows(named=True)
41 + ],
42 + )
43 +
44 +
45 +@router.get("/geographies/coverage")
46 +def coverage_matrix(
47 + request: Request,
48 + response: Response,
49 + status: str | None = Query(None, pattern="^(published|conditional|not_published)$"),
50 + format: str = Query("json", pattern="^(json|csv)$"),
51 +):
52 + """Full municipality × type coverage matrix with publication status."""
53 + df = data.coverage()
54 + if status:
55 + df = df.filter(pl.col("coverage_status") == status)
56 + if format == "csv":
57 + return csv_response(df)
58 + if etag_for(request, response, str(status)):
59 + return Response(status_code=304)
60 + return {
61 + "coverage": df.rename({"propertyType": "property_type"}).to_dicts(),
62 + "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai",
63 + }
added api/app/routers/index.py +197 −0
@@ -0,0 +1,197 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/index.py
6 +# Purpose : Index endpoints (monthly v2.1) — series, latest, compare, vintages.
7 +# =============================================================================
8 +"""Index series endpoints."""
9 +
10 +from __future__ import annotations
11 +
12 +import math
13 +
14 +import polars as pl
15 +from fastapi import APIRouter, HTTPException, Query, Request, Response
16 +
17 +from app.deps import csv_response, etag_for, validate_series
18 +from app.schemas import LatestResponse, Observation, SeriesResponse, VintageObservation
19 +from app.services import data
20 +
21 +router = APIRouter()
22 +
23 +
24 +def _clean(v):
25 + return None if (v is None or (isinstance(v, float) and math.isnan(v))) else v
26 +
27 +
28 +def _obs(row: dict) -> Observation:
29 + return Observation(
30 + period=row["period"],
31 + index=_clean(row["index"]),
32 + index_smoothed=row["index_smoothed"],
33 + representative_value=row["representative_value"],
34 + transactions=row["transactions"],
35 + effective_sample_size=row["effective_sample_size"],
36 + monthly_pct=row["monthly_pct"],
37 + three_month_pct=row["three_month_pct"],
38 + six_month_pct=row["six_month_pct"],
39 + yoy_pct=row["yoy_pct"],
40 + lower_95=row["lower_95"],
41 + upper_95=row["upper_95"],
42 + reliability_grade=row["reliability_grade"],
43 + shrinkage_weight=row["shrinkage_weight"],
44 + is_partial_month=row["is_partial_month"],
45 + )
46 +
47 +
48 +@router.get("/index", response_model=SeriesResponse)
49 +def get_index(
50 + request: Request,
51 + response: Response,
52 + geography: str = Query(..., description="geography_id, e.g. quebec, region-06, montreal"),
53 + type: str = Query("all", alias="type"),
54 + from_: str | None = Query(None, alias="from", description="first period (YYYY-MM)"),
55 + to: str | None = Query("latest"),
56 + limit: int | None = Query(None, ge=1, le=1000),
57 + offset: int = Query(0, ge=0),
58 + format: str = Query("json", pattern="^(json|csv)$"),
59 +):
60 + """Full monthly history for one series (paginated; CSV via ?format=csv)."""
61 + validate_series(geography, type)
62 + df = data.series(geography, type, from_, to)
63 + if df.height == 0:
64 + raise HTTPException(404, "no observations for this series/range")
65 + total = df.height
66 + page = df.slice(offset, limit) if limit else df.slice(offset)
67 + if format == "csv":
68 + return csv_response(page)
69 + if etag_for(request, response, geography, type, str(from_), str(to),
70 + str(limit), str(offset)):
71 + return Response(status_code=304)
72 + first = page.row(0, named=True)
73 + return SeriesResponse(
74 + geography=geography,
75 + geography_name=first["geography_name"],
76 + geography_level=first["geography_level"],
77 + property_type=type,
78 + model_version=first["model_version"],
79 + data_vintage=first["data_vintage"],
80 + total_observations=total,
81 + offset=offset,
82 + limit=limit,
83 + observations=[_obs(r) for r in page.iter_rows(named=True)],
84 + )
85 +
86 +
87 +@router.get("/index/latest", response_model=LatestResponse)
88 +def get_latest(
89 + request: Request,
90 + response: Response,
91 + geography: str,
92 + type: str = "all",
93 + include_partial: bool = Query(False, description="include the partial (nowcast) month"),
94 +):
95 + """Latest observation (complete month by default; nowcast on request)."""
96 + validate_series(geography, type)
97 + df = data.series(geography, type)
98 + if not include_partial:
99 + df = df.filter(~pl.col("is_partial_month"))
100 + if df.height == 0:
101 + raise HTTPException(404, "no observations")
102 + row = df.row(df.height - 1, named=True)
103 + if etag_for(request, response, geography, type, row["period"],
104 + str(include_partial)):
105 + return Response(status_code=304)
106 + return LatestResponse(
107 + geography=geography,
108 + geography_name=row["geography_name"],
109 + property_type=type,
110 + period=row["period"],
111 + latest_index=round(row["index_smoothed"], 2),
112 + latest_index_raw=(None if _clean(row["index"]) is None
113 + else round(row["index"], 2)),
114 + representative_value=row["representative_value"],
115 + monthly_change=row["monthly_pct"],
116 + three_month_change=row["three_month_pct"],
117 + yoy_change=row["yoy_pct"],
118 + transactions=row["transactions"],
119 + effective_sample_size=row["effective_sample_size"],
120 + reliability=row["reliability_grade"],
121 + lower_95=round(row["lower_95"], 2),
122 + upper_95=round(row["upper_95"], 2),
123 + is_partial_month=row["is_partial_month"],
124 + model_version=row["model_version"],
125 + data_vintage=row["data_vintage"],
126 + )
127 +
128 +
129 +@router.get("/compare")
130 +def compare(
131 + request: Request,
132 + response: Response,
133 + series: str = Query(..., description="comma list of geography:type, e.g. "
134 + "montreal:condo,quebec-city:condo"),
135 + rebase_period: str | None = Query(None, description="rebase all series to 100 at this period"),
136 +):
137 + """Aligned comparison of up to 8 series, optionally rebased."""
138 + pairs = [s.strip() for s in series.split(",") if s.strip()]
139 + if not 2 <= len(pairs) <= 8:
140 + raise HTTPException(422, "provide 2 to 8 series")
141 + out = {}
142 + for p in pairs:
143 + try:
144 + g, t = p.split(":")
145 + except ValueError:
146 + raise HTTPException(422, f"bad series spec '{p}' (want geography:type)")
147 + validate_series(g, t)
148 + df = data.series(g, t).select("period", "index_smoothed",
149 + "representative_value", "reliability_grade")
150 + if rebase_period:
151 + base = df.filter(pl.col("period") == rebase_period)
152 + if base.height == 0:
153 + raise HTTPException(422, f"rebase_period {rebase_period} not found for {p}")
154 + df = df.with_columns(
155 + (pl.col("index_smoothed") / base["index_smoothed"][0] * 100)
156 + .round(3).alias("index_smoothed"))
157 + out[p] = df.to_dicts()
158 + if etag_for(request, response, series, str(rebase_period)):
159 + return Response(status_code=304)
160 + return {"series": out, "rebase_period": rebase_period,
161 + "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}
162 +
163 +
164 +@router.get("/vintages")
165 +def vintages(
166 + geography: str,
167 + type: str = "all",
168 + period: str | None = None,
169 +):
170 + """First release vs current vintage per period (revision tracking)."""
171 + validate_series(geography, type)
172 + fr = data.first_release()
173 + if fr is None:
174 + raise HTTPException(404, "no vintage history yet")
175 + cur = data.series(geography, type).select(
176 + "period", "index_smoothed", "data_vintage")
177 + j = (
178 + fr.filter((pl.col("geography_id") == geography)
179 + & (pl.col("property_type") == type))
180 + .join(cur, on="period", how="inner")
181 + )
182 + if period:
183 + j = j.filter(pl.col("period") == period)
184 + obs = [
185 + VintageObservation(
186 + period=r["period"],
187 + first_release_index_smoothed=round(r["first_release_index_smoothed"], 3),
188 + current_index_smoothed=round(r["index_smoothed"], 3),
189 + first_release_vintage=r["first_release_vintage"],
190 + current_vintage=r["data_vintage"],
191 + revision_pct=round((r["index_smoothed"]
192 + / r["first_release_index_smoothed"] - 1) * 100, 3),
193 + ).model_dump()
194 + for r in j.sort("period").iter_rows(named=True)
195 + ]
196 + return {"geography": geography, "property_type": type, "observations": obs,
197 + "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}
added api/app/routers/liquidity.py +41 −0
@@ -0,0 +1,41 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/liquidity.py
6 +# Purpose : Weekly transaction-volume (liquidity) endpoint per series.
7 +# =============================================================================
8 +"""Liquidity endpoints."""
9 +
10 +from __future__ import annotations
11 +
12 +import polars as pl
13 +from fastapi import APIRouter, HTTPException, Request, Response
14 +
15 +from app.deps import etag_for, validate_series
16 +from app.schemas import LiquidityPoint, LiquidityResponse
17 +from app.services import data
18 +
19 +router = APIRouter()
20 +
21 +
22 +@router.get("/liquidity", response_model=LiquidityResponse)
23 +def liquidity(request: Request, response: Response,
24 + geography: str, type: str = "all"):
25 + """Weekly transaction counts for one series."""
26 + validate_series(geography, type)
27 + df = data.liquidity().filter(
28 + (pl.col("geography_id") == geography)
29 + & (pl.col("property_type") == type)
30 + ).sort("period")
31 + if df.height == 0:
32 + raise HTTPException(404, "no liquidity data")
33 + if etag_for(request, response, geography, type):
34 + return Response(status_code=304)
35 + return LiquidityResponse(
36 + geography=geography,
37 + property_type=type,
38 + median_monthly_transactions=float(df["transactions"].median()),
39 + observations=[LiquidityPoint(period=r["period"], transactions=r["transactions"])
40 + for r in df.iter_rows(named=True)],
41 + )
added api/app/routers/maps.py +72 −0
@@ -0,0 +1,72 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/maps.py
6 +# Purpose : Choropleth data endpoint — one metric across geographies.
7 +# =============================================================================
8 +"""Map data endpoints (region choropleth + assessment gap)."""
9 +
10 +from __future__ import annotations
11 +
12 +import polars as pl
13 +from fastapi import APIRouter, HTTPException, Query, Request, Response
14 +
15 +from app.deps import etag_for
16 +from app.schemas import MapCell, MapResponse
17 +from app.services import data
18 +
19 +router = APIRouter()
20 +
21 +METRICS = {
22 + "yoy": "yoy_pct",
23 + "six_month": "six_month_pct",
24 + "three_month": "three_month_pct",
25 + "index_level": "index_smoothed",
26 +}
27 +
28 +
29 +@router.get("/map", response_model=MapResponse)
30 +def map_data(
31 + request: Request,
32 + response: Response,
33 + metric: str = Query("yoy", description=f"one of {sorted(METRICS)} or assessment_gap"),
34 + level: str = Query("region", pattern="^(region|municipality)$"),
35 + type: str = Query("all"),
36 + period: str | None = None,
37 +):
38 + """One metric per geography for choropleth rendering."""
39 + c = data.canonical().filter(
40 + (pl.col("geography_level") == level) & (pl.col("property_type") == type))
41 + if period is None:
42 + period = c.filter(~pl.col("is_partial_month"))["period"].max()
43 + if metric == "assessment_gap":
44 + gap = data.assessment_gap().filter(
45 + (pl.col("property_type") == type) & (pl.col("period") == period)
46 + & pl.col("geography_id").str.starts_with("region-" if level == "region" else ""))
47 + c_wk = c.filter(pl.col("period") == period).select(
48 + "geography_id", "geography_name", "reliability_grade", "transactions")
49 + j = c_wk.join(gap, on="geography_id", how="left")
50 + cells = [MapCell(geography_id=r["geography_id"],
51 + geography_name=r["geography_name"],
52 + value=r["median_ratio"],
53 + reliability_grade=r["reliability_grade"],
54 + transactions=r["transactions"])
55 + for r in j.iter_rows(named=True)]
56 + else:
57 + if metric not in METRICS:
58 + raise HTTPException(422, f"metric must be one of {sorted(METRICS)} or assessment_gap")
59 + col = METRICS[metric]
60 + wk = c.filter(pl.col("period") == period)
61 + if wk.height == 0:
62 + raise HTTPException(404, f"no data for period {period}")
63 + cells = [MapCell(geography_id=r["geography_id"],
64 + geography_name=r["geography_name"],
65 + value=r[col],
66 + reliability_grade=r["reliability_grade"],
67 + transactions=r["transactions"])
68 + for r in wk.iter_rows(named=True)]
69 + if etag_for(request, response, metric, level, type, str(period)):
70 + return Response(status_code=304)
71 + return MapResponse(metric=metric, level=level, property_type=type,
72 + period=str(period), cells=cells)
added api/app/routers/meta.py +41 −0
@@ -0,0 +1,41 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/meta.py
6 +# Purpose : /v1/meta and /health endpoints.
7 +# =============================================================================
8 +"""Metadata and health endpoints."""
9 +
10 +from __future__ import annotations
11 +
12 +import polars as pl
13 +from fastapi import APIRouter
14 +
15 +from app.schemas import MetaResponse
16 +from app.services import data
17 +
18 +router = APIRouter()
19 +
20 +
21 +@router.get("/v1/meta", response_model=MetaResponse)
22 +def meta():
23 + c = data.canonical()
24 + return MetaResponse(
25 + model_version=data.model_version(),
26 + data_vintage=data.data_vintage(),
27 + series_count=c.select("geography_id", "property_type").unique().height,
28 + periods=c["period"].n_unique(),
29 + latest_complete_period=str(
30 + c.filter(~pl.col("is_partial_month"))["period"].max()),
31 + )
32 +
33 +
34 +@router.get("/health")
35 +def health():
36 + try:
37 + rows = data.canonical().height
38 + return {"status": "ok", "canonical_rows": rows,
39 + "data_vintage": data.data_vintage()}
40 + except Exception as exc: # pragma: no cover
41 + return {"status": "degraded", "error": str(exc)}
added api/app/routers/reports.py +89 −0
@@ -0,0 +1,89 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/routers/reports.py
6 +# Purpose : Stats + automatic PDF report endpoints.
7 +# =============================================================================
8 +"""Derived statistics and report-generation endpoints."""
9 +
10 +from __future__ import annotations
11 +
12 +from fastapi import APIRouter, HTTPException, Query, Request, Response
13 +
14 +from app.deps import etag_for, validate_series
15 +from app.services import data, report, stats
16 +
17 +router = APIRouter()
18 +
19 +
20 +@router.get("/stats")
21 +def series_statistics(request: Request, response: Response,
22 + geography: str, type: str = "all"):
23 + """Rich derived metrics for one series (peak, drawdown, momentum, ...)."""
24 + validate_series(geography, type)
25 + st = stats.series_stats(geography, type)
26 + if not st:
27 + raise HTTPException(404, "series too short for statistics")
28 + if etag_for(request, response, geography, type, "stats"):
29 + return Response(status_code=304)
30 + return {"geography": geography, "property_type": type, **st,
31 + "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}
32 +
33 +
34 +@router.get("/stats/overview")
35 +def statistics_overview(request: Request, response: Response,
36 + level: str = Query("region", pattern="^(region|municipality)$"),
37 + type: str = "all"):
38 + """One rich row per geography — powers heatmap tables."""
39 + if etag_for(request, response, level, type, "overview"):
40 + return Response(status_code=304)
41 + return {**stats.overview_rows(level, type),
42 + "author": "Simon-Pierre Boucher", "contact": "contact@spboucher.ai"}
43 +
44 +
45 +def _parse_specs(series: str) -> list[tuple[str, str]]:
46 + specs = []
47 + for p in [s.strip() for s in series.split(",") if s.strip()]:
48 + try:
49 + g, t = p.split(":")
50 + except ValueError:
51 + raise HTTPException(422, f"bad series spec '{p}' (want geography:type)")
52 + validate_series(g, t)
53 + specs.append((g, t))
54 + if not 1 <= len(specs) <= 5:
55 + raise HTTPException(422, "provide 1 to 5 series")
56 + return specs
57 +
58 +
59 +@router.get("/report")
60 +def series_report(
61 + series: str = Query(..., description="comma list of geography:type (1-5)"),
62 + format: str = Query("pdf", pattern="^(pdf|json)$"),
63 +):
64 + """Automatic report: one page per series (+ comparison page if several)."""
65 + specs = _parse_specs(series)
66 + if format == "json":
67 + return {f"{g}:{t}": stats.series_stats(g, t) for g, t in specs}
68 + pdf = report.build_series_report(specs)
69 + fname = "qhpi_report_" + "_".join(f"{g}-{t}" for g, t in specs) + ".pdf"
70 + return Response(
71 + content=pdf, media_type="application/pdf",
72 + headers={
73 + "Content-Disposition": f'inline; filename="{fname}"',
74 + "Cache-Control": "public, max-age=300",
75 + "X-Author": "Simon-Pierre Boucher <contact@spboucher.ai>",
76 + })
77 +
78 +
79 +@router.get("/report/market")
80 +def market_report(type: str = Query("all", pattern="^(all|unifamilial|condo|plex)$")):
81 + """Full market report: province, 17-region pulse table, property types."""
82 + pdf = report.build_market_report(type)
83 + return Response(
84 + content=pdf, media_type="application/pdf",
85 + headers={
86 + "Content-Disposition": f'inline; filename="qhpi_market_report_{type}.pdf"',
87 + "Cache-Control": "public, max-age=300",
88 + "X-Author": "Simon-Pierre Boucher <contact@spboucher.ai>",
89 + })
added api/app/schemas.py +148 −0
@@ -0,0 +1,148 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/schemas.py
6 +# Purpose : Pydantic response schemas (monthly v2.1) — uncertainty and
7 +# reliability are always present, never hidden.
8 +# =============================================================================
9 +"""API response schemas — QHPI monthly."""
10 +
11 +from __future__ import annotations
12 +
13 +from pydantic import BaseModel
14 +
15 +AUTHOR = "Simon-Pierre Boucher"
16 +CONTACT = "contact@spboucher.ai"
17 +
18 +
19 +class Credit(BaseModel):
20 + author: str = AUTHOR
21 + contact: str = CONTACT
22 +
23 +
24 +class Observation(BaseModel):
25 + period: str
26 + index: float | None
27 + index_smoothed: float
28 + representative_value: float | None
29 + transactions: int
30 + effective_sample_size: float | None
31 + monthly_pct: float | None
32 + three_month_pct: float | None
33 + six_month_pct: float | None
34 + yoy_pct: float | None
35 + lower_95: float
36 + upper_95: float
37 + reliability_grade: str
38 + shrinkage_weight: float | None
39 + is_partial_month: bool
40 +
41 +
42 +class SeriesResponse(Credit):
43 + geography: str
44 + geography_name: str
45 + geography_level: str
46 + property_type: str
47 + frequency: str = "monthly"
48 + base_period: str = "2021 average = 100"
49 + model_version: str
50 + data_vintage: str
51 + total_observations: int
52 + offset: int = 0
53 + limit: int | None = None
54 + observations: list[Observation]
55 +
56 +
57 +class LatestResponse(Credit):
58 + geography: str
59 + geography_name: str
60 + property_type: str
61 + frequency: str = "monthly"
62 + period: str
63 + latest_index: float
64 + latest_index_raw: float | None
65 + representative_value: float | None
66 + monthly_change: float | None
67 + three_month_change: float | None
68 + yoy_change: float | None
69 + transactions: int
70 + effective_sample_size: float | None
71 + reliability: str
72 + lower_95: float
73 + upper_95: float
74 + is_partial_month: bool
75 + model_version: str
76 + data_vintage: str
77 +
78 +
79 +class GeographyNode(BaseModel):
80 + geography_id: str
81 + geography_name: str
82 + geography_level: str
83 + property_types: list[str]
84 +
85 +
86 +class GeographiesResponse(Credit):
87 + data_vintage: str
88 + published_series: list[GeographyNode]
89 + coverage_matrix_url: str = "/v1/geographies/coverage"
90 +
91 +
92 +class LiquidityPoint(BaseModel):
93 + period: str
94 + transactions: int
95 +
96 +
97 +class LiquidityResponse(Credit):
98 + geography: str
99 + property_type: str
100 + median_monthly_transactions: float
101 + observations: list[LiquidityPoint]
102 +
103 +
104 +class MapCell(BaseModel):
105 + geography_id: str
106 + geography_name: str
107 + value: float | None
108 + reliability_grade: str
109 + transactions: int
110 +
111 +
112 +class MapResponse(Credit):
113 + metric: str
114 + level: str
115 + property_type: str
116 + period: str
117 + cells: list[MapCell]
118 +
119 +
120 +class VintageObservation(BaseModel):
121 + period: str
122 + first_release_index_smoothed: float
123 + current_index_smoothed: float
124 + first_release_vintage: str
125 + current_vintage: str
126 + revision_pct: float
127 +
128 +
129 +class MetaResponse(Credit):
130 + name: str = "QHPI — Quebec Housing Price Index (monthly)"
131 + model_version: str
132 + data_vintage: str
133 + frequency: str = "monthly"
134 + base_period: str = "2021 average = 100"
135 + methodology: str = (
136 + "Robust monthly architecture: province paths by rolling-time-dummy "
137 + "hedonic (13-month Huber-IRLS windows, mean splice — revision-free); "
138 + "liquid cells (≥40 tx/month) estimated directly by local robust "
139 + "time-dummy with state-space smoothing; thin cells shrunk toward "
140 + "their parent path via a heteroskedastic local-level Kalman model. "
141 + "Validated against repeat sales, stratified matched-cell medians, "
142 + "downsampling and composition-shock simulations."
143 + )
144 + methodology_url: str = "/methodology"
145 + seasonal_adjustment: str = "NSA (no stable monthly seasonality detected)"
146 + series_count: int
147 + periods: int
148 + latest_complete_period: str
added api/app/services/__init__.py +7 −0
@@ -0,0 +1,7 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/services/__init__.py
6 +# Purpose : Services package for the QWHPI API.
7 +# =============================================================================
added api/app/services/data.py +96 −0
@@ -0,0 +1,96 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/services/data.py
6 +# Purpose : Data access layer — canonical parquet lake (source of truth),
7 +# in-memory cached, vintage-keyed for ETag support.
8 +# =============================================================================
9 +"""Data access for the API.
10 +
11 +Reads the processed Parquet lake (never raw CSVs). Frames are cached in
12 +memory and refreshed when the canonical file's mtime changes (the weekly
13 +scheduler swaps files atomically). ``DATA_DIR`` can be overridden via the
14 +``QWHPI_DATA_DIR`` environment variable (used by docker-compose).
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import os
20 +import threading
21 +from pathlib import Path
22 +
23 +import polars as pl
24 +
25 +_REPO_ROOT = Path(__file__).resolve().parents[3]
26 +DATA_DIR = Path(os.environ.get("QWHPI_DATA_DIR", _REPO_ROOT / "data" / "processed"))
27 +
28 +CANONICAL = DATA_DIR / "qhpi_monthly.parquet"
29 +COVERAGE = DATA_DIR / "coverage_matrix_monthly.parquet"
30 +LIQUIDITY = DATA_DIR / "monthly_liquidity.parquet"
31 +ASSESSMENT = DATA_DIR / "assessment_gap_monthly.parquet"
32 +MUNICIPALITIES = DATA_DIR / "municipalities.parquet"
33 +FIRST_RELEASE = DATA_DIR / "first_release_monthly.parquet"
34 +
35 +_lock = threading.Lock()
36 +_cache: dict[str, tuple[float, pl.DataFrame]] = {}
37 +
38 +
39 +def _load(path: Path) -> pl.DataFrame:
40 + mtime = path.stat().st_mtime
41 + with _lock:
42 + hit = _cache.get(str(path))
43 + if hit and hit[0] == mtime:
44 + return hit[1]
45 + df = pl.read_parquet(path)
46 + _cache[str(path)] = (mtime, df)
47 + return df
48 +
49 +
50 +def canonical() -> pl.DataFrame:
51 + return _load(CANONICAL)
52 +
53 +
54 +def coverage() -> pl.DataFrame:
55 + return _load(COVERAGE)
56 +
57 +
58 +def liquidity() -> pl.DataFrame:
59 + return _load(LIQUIDITY)
60 +
61 +
62 +def assessment_gap() -> pl.DataFrame:
63 + return _load(ASSESSMENT)
64 +
65 +
66 +def municipalities() -> pl.DataFrame:
67 + return _load(MUNICIPALITIES)
68 +
69 +
70 +def first_release() -> pl.DataFrame | None:
71 + return _load(FIRST_RELEASE) if FIRST_RELEASE.exists() else None
72 +
73 +
74 +def data_vintage() -> str:
75 + return str(canonical()["data_vintage"][0])
76 +
77 +
78 +def model_version() -> str:
79 + return str(canonical()["model_version"][0])
80 +
81 +
82 +def series(geography: str, property_type: str,
83 + period_from: str | None = None, period_to: str | None = None) -> pl.DataFrame:
84 + df = canonical().filter(
85 + (pl.col("geography_id") == geography)
86 + & (pl.col("property_type") == property_type)
87 + )
88 + if period_from:
89 + df = df.filter(pl.col("period") >= period_from)
90 + if period_to and period_to != "latest":
91 + df = df.filter(pl.col("period") <= period_to)
92 + return df.sort("period")
93 +
94 +
95 +def geography_ids() -> list[str]:
96 + return canonical()["geography_id"].unique().sort().to_list()
added api/app/services/report.py +345 −0
@@ -0,0 +1,345 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/services/report.py
6 +# Purpose : Automatic PDF report generation — single series, multi-series
7 +# comparison, and full market report (matplotlib, palette-styled).
8 +# =============================================================================
9 +"""PDF report factory.
10 +
11 +Every report is generated on demand from the canonical monthly parquet:
12 +- series report ....... hero metrics grid, index chart with CI band, dollar
13 + value chart, volume bars, growth & risk table
14 +- comparison page ..... rebased multi-series chart + side-by-side table
15 +- market report ....... Quebec overview, regional heatmap table, type panel
16 +
17 +Styling matches the dashboard palette; every page carries the author credit.
18 +"""
19 +
20 +from __future__ import annotations
21 +
22 +import io
23 +from datetime import date
24 +
25 +import matplotlib
26 +
27 +matplotlib.use("Agg")
28 +import matplotlib.pyplot as plt # noqa: E402
29 +import numpy as np # noqa: E402
30 +import polars as pl # noqa: E402
31 +from matplotlib.backends.backend_pdf import PdfPages # noqa: E402
32 +
33 +from app.services import data, stats # noqa: E402
34 +
35 +# Palette (mirrors web/styles/globals.css light mode)
36 +BLUE, ORANGE, AQUA, YELLOW, MAGENTA = "#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4"
37 +SERIES_COLORS = [BLUE, ORANGE, AQUA, YELLOW, MAGENTA]
38 +INK, INK2, MUTED = "#0b0b0b", "#52514e", "#8a887f"
39 +SURFACE, BORDER, BAND = "#fcfcfb", "#dddcd6", (42 / 255, 120 / 255, 214 / 255, 0.14)
40 +GOOD, BAD = "#008300", "#e34948"
41 +
42 +plt.rcParams.update({
43 + "figure.facecolor": SURFACE, "axes.facecolor": SURFACE,
44 + "axes.edgecolor": BORDER, "axes.grid": True, "grid.color": BORDER,
45 + "grid.linestyle": ":", "grid.linewidth": 0.7,
46 + "axes.spines.top": False, "axes.spines.right": False,
47 + "text.color": INK, "axes.labelcolor": INK2,
48 + "xtick.color": INK2, "ytick.color": INK2, "font.size": 9,
49 +})
50 +
51 +PAGE = (8.27, 11.69) # A4 portrait
52 +
53 +
54 +def _fmt_pct(v, plus=True):
55 + if v is None:
56 + return "—"
57 + s = f"{v:+.2f}%" if plus else f"{v:.2f}%"
58 + return s
59 +
60 +
61 +def _fmt_money(v):
62 + return "—" if v is None else f"${v:,.0f}"
63 +
64 +
65 +def _footer(fig, page_note=""):
66 + fig.text(0.05, 0.02, "QHPI — Quebec Housing Price Index · Simon-Pierre Boucher · contact@spboucher.ai",
67 + fontsize=7, color=MUTED)
68 + fig.text(0.95, 0.02, page_note, fontsize=7, color=MUTED, ha="right")
69 +
70 +
71 +def _title_block(fig, title, subtitle):
72 + fig.text(0.05, 0.955, title, fontsize=19, fontweight="bold", color=INK)
73 + fig.text(0.05, 0.928, subtitle, fontsize=9.5, color=INK2)
74 + fig.add_artist(plt.Line2D([0.05, 0.95], [0.915, 0.915],
75 + color=BORDER, lw=1, transform=fig.transFigure))
76 +
77 +
78 +def _metric_cells(fig, y_top, items, ncols=4):
79 + """Grid of metric tiles: items = [(label, value, color?)]"""
80 + n = len(items)
81 + nrows = (n + ncols - 1) // ncols
82 + w = 0.9 / ncols
83 + h = 0.055
84 + for i, item in enumerate(items):
85 + label, value = item[0], item[1]
86 + color = item[2] if len(item) > 2 else INK
87 + r, c = divmod(i, ncols)
88 + x = 0.05 + c * w
89 + y = y_top - r * (h + 0.018)
90 + fig.text(x, y, label.upper(), fontsize=6.7, color=MUTED)
91 + fig.text(x, y - 0.026, value, fontsize=13, fontweight="bold", color=color)
92 + return y_top - nrows * (h + 0.018)
93 +
94 +
95 +def _delta_color(v):
96 + if v is None:
97 + return INK
98 + return GOOD if v >= 0 else BAD
99 +
100 +
101 +def _index_axes(ax, df, unit="points", legend=True):
102 + periods = df["period"].to_list()
103 + x = np.arange(len(periods))
104 + k = 1.0
105 + if unit == "dollars":
106 + row = df.filter(pl.col("representative_value").is_not_null())
107 + k = row["representative_value"][0] / row["index_smoothed"][0]
108 + smoothed = df["index_smoothed"].to_numpy() * k
109 + lo = df["lower_95"].to_numpy() * k
110 + hi = df["upper_95"].to_numpy() * k
111 + raw = df["index"].to_numpy() * k
112 + ax.fill_between(x, lo, hi, color=BAND, label="95% CI", linewidth=0)
113 + ax.plot(x, raw, color=BLUE, alpha=0.35, lw=0.8, label="raw monthly")
114 + ax.plot(x, smoothed, color=BLUE, lw=1.9, label="index (smoothed)")
115 + pad = (smoothed.max() - smoothed.min()) * 0.15 or 1
116 + ax.set_ylim(min(smoothed.min(), np.nanmin(raw)) - pad,
117 + max(smoothed.max(), np.nanmax(raw)) + pad)
118 + step = max(1, len(x) // 8)
119 + ax.set_xticks(x[::step], [p for p in periods[::step]], fontsize=7.5)
120 + if unit == "dollars":
121 + ax.yaxis.set_major_formatter(lambda v, _: f"${v / 1000:.0f}k")
122 + if legend:
123 + ax.legend(loc="upper left", fontsize=7.5, frameon=False)
124 +
125 +
126 +def _volume_axes(ax, df):
127 + x = np.arange(df.height)
128 + ax.bar(x, df["transactions"].to_numpy(), color=BLUE, alpha=0.55, width=0.85)
129 + ax.set_xticks([])
130 + ax.tick_params(labelsize=6.5)
131 + ax.set_title("Transactions per month", fontsize=8, loc="left", color=INK2)
132 +
133 +
134 +def _series_page(pdf, gid, ptype):
135 + df = data.series(gid, ptype).filter(~pl.col("is_partial_month"))
136 + st = stats.series_stats(gid, ptype)
137 + name = df["geography_name"][0]
138 + fig = plt.figure(figsize=PAGE)
139 + _title_block(fig, f"{name}{ptype}",
140 + f"Monthly housing price index · base 2021 = 100 · data to {st['period']} · "
141 + f"reliability {st['reliability_grade']} · generated {date.today().isoformat()}")
142 +
143 + y = _metric_cells(fig, 0.875, [
144 + ("Index", f"{st['index']:.1f}"),
145 + ("Dollar value", _fmt_money(st["representative_value"])),
146 + ("1 month", _fmt_pct(st["monthly_pct"]), _delta_color(st["monthly_pct"])),
147 + ("Year over year", _fmt_pct(st["yoy_pct"]), _delta_color(st["yoy_pct"])),
148 + ("3 months", _fmt_pct(st["three_month_pct"]), _delta_color(st["three_month_pct"])),
149 + ("6 months", _fmt_pct(st["six_month_pct"]), _delta_color(st["six_month_pct"])),
150 + ("Since 2021", _fmt_pct(st["since_2021_pct"]), _delta_color(st["since_2021_pct"])),
151 + ("CAGR", _fmt_pct(st["cagr_pct"]), _delta_color(st["cagr_pct"])),
152 + ("Peak", f"{st['peak_index']:.1f} ({st['peak_period']})"),
153 + ("Vs peak", _fmt_pct(st["drawdown_pct"]),
154 + GOOD if st["at_record_high"] else BAD),
155 + ("Volatility (12m)", _fmt_pct(st["volatility_12m_pct"], plus=False)),
156 + ("Momentum (3m ann.)", _fmt_pct(st["momentum_3m_ann_pct"]),
157 + _delta_color(st["momentum_3m_ann_pct"])),
158 + ("Sales (12m)", f"{st['volume_12m']:,}"),
159 + ("Volume YoY", _fmt_pct(st["volume_yoy_pct"]),
160 + _delta_color(st["volume_yoy_pct"])),
161 + ("$ volume (12m, est.)", f"${st['dollar_volume_12m'] / 1e6:,.0f}M"),
162 + ("YoY rank", f"{st['yoy_rank']}/{st['yoy_rank_of']}" if st["yoy_rank"] else "—"),
163 + ])
164 +
165 + del y # metrics grid occupies down to ~0.56; charts use fixed slots
166 + ax1 = fig.add_axes([0.07, 0.345, 0.87, 0.185])
167 + _index_axes(ax1, df)
168 + ax1.set_title("Index (2021 = 100) with 95% confidence band",
169 + fontsize=9, loc="left", color=INK2)
170 +
171 + ax2 = fig.add_axes([0.07, 0.125, 0.87, 0.165])
172 + _index_axes(ax2, df, unit="dollars", legend=False)
173 + ax2.set_title("Representative dollar value (fixed 2021 basket)",
174 + fontsize=9, loc="left", color=INK2)
175 +
176 + ax3 = fig.add_axes([0.07, 0.042, 0.87, 0.05])
177 + _volume_axes(ax3, df)
178 +
179 + _footer(fig, f"{gid}:{ptype}")
180 + pdf.savefig(fig)
181 + plt.close(fig)
182 +
183 +
184 +def _comparison_page(pdf, specs):
185 + fig = plt.figure(figsize=PAGE)
186 + _title_block(fig, "Comparison", "All series rebased to 100 at the first common month")
187 + frames = {}
188 + for gid, ptype in specs:
189 + frames[(gid, ptype)] = data.series(gid, ptype).filter(~pl.col("is_partial_month"))
190 + common = sorted(set.intersection(*[set(f["period"].to_list()) for f in frames.values()]))
191 + base_p = common[0]
192 +
193 + ax = fig.add_axes([0.07, 0.45, 0.87, 0.42])
194 + rows = []
195 + for i, ((gid, ptype), f) in enumerate(frames.items()):
196 + f = f.filter(pl.col("period").is_in(common)).sort("period")
197 + idx = f["index_smoothed"].to_numpy()
198 + rebased = idx / idx[0] * 100
199 + x = np.arange(len(common))
200 + name = f["geography_name"][0]
201 + ax.plot(x, rebased, color=SERIES_COLORS[i % 5], lw=1.9, label=f"{name} · {ptype}")
202 + st = stats.series_stats(gid, ptype)
203 + rows.append([f"{name} · {ptype}", f"{st['index']:.1f}",
204 + _fmt_pct(st["yoy_pct"]), _fmt_pct(st["since_2021_pct"]),
205 + _fmt_money(st["representative_value"]), st["reliability_grade"]])
206 + step = max(1, len(common) // 8)
207 + ax.set_xticks(np.arange(len(common))[::step], common[::step], fontsize=7.5)
208 + ax.legend(loc="upper left", fontsize=8, frameon=False)
209 + ax.set_title(f"Rebased to 100 at {base_p}", fontsize=9, loc="left", color=INK2)
210 +
211 + tbl_ax = fig.add_axes([0.07, 0.13, 0.87, 0.24])
212 + tbl_ax.axis("off")
213 + table = tbl_ax.table(
214 + cellText=rows,
215 + colLabels=["Series", "Index", "YoY", "Since 2021", "$ value", "Grade"],
216 + loc="upper center", cellLoc="center")
217 + table.auto_set_font_size(False)
218 + table.set_fontsize(8.5)
219 + table.scale(1, 1.6)
220 + for (r, c), cell in table.get_celld().items():
221 + cell.set_edgecolor(BORDER)
222 + if r == 0:
223 + cell.set_text_props(color=INK2, fontweight="bold")
224 + cell.set_facecolor("#f0efec")
225 + _footer(fig, "comparison")
226 + pdf.savefig(fig)
227 + plt.close(fig)
228 +
229 +
230 +def build_series_report(specs: list[tuple[str, str]]) -> bytes:
231 + buf = io.BytesIO()
232 + with PdfPages(buf) as pdf:
233 + for gid, ptype in specs:
234 + _series_page(pdf, gid, ptype)
235 + if len(specs) > 1:
236 + _comparison_page(pdf, specs)
237 + meta = pdf.infodict()
238 + meta["Title"] = "QHPI report — " + ", ".join(f"{g}:{t}" for g, t in specs)
239 + meta["Author"] = "Simon-Pierre Boucher <contact@spboucher.ai>"
240 + return buf.getvalue()
241 +
242 +
243 +def build_market_report(ptype: str = "all") -> bytes:
244 + buf = io.BytesIO()
245 + with PdfPages(buf) as pdf:
246 + # Page 1 — province
247 + _series_page(pdf, "quebec", ptype)
248 +
249 + # Page 2 — regional heatmap table
250 + ov = stats.overview_rows("region", ptype)
251 + fig = plt.figure(figsize=PAGE)
252 + _title_block(fig, "Regions — market pulse",
253 + f"All 17 administrative regions · {ptype} · month of {ov['period']}")
254 + rows, colors = [], []
255 + vals = [r["yoy_pct"] for r in ov["rows"] if r["yoy_pct"] is not None]
256 + lo, hi = min(vals), max(vals)
257 +
258 + def heat(v):
259 + if v is None:
260 + return SURFACE
261 + t = 0.5 if hi == lo else (v - lo) / (hi - lo)
262 + ramp = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf"]
263 + return ramp[min(4, int(t * 5))]
264 +
265 + for r in ov["rows"]:
266 + rows.append([r["geography_name"][:28], f"{r['index']:.1f}",
267 + _fmt_pct(r["monthly_pct"]), _fmt_pct(r["three_month_pct"]),
268 + _fmt_pct(r["six_month_pct"]), _fmt_pct(r["yoy_pct"]),
269 + _fmt_pct(r["drawdown_pct"]), f"{r['volume_12m']:,}",
270 + r["reliability_grade"]])
271 + colors.append(heat(r["yoy_pct"]))
272 + ax = fig.add_axes([0.05, 0.44, 0.9, 0.44])
273 + ax.axis("off")
274 + widths = [0.26] + [0.0925] * 8
275 + table = ax.table(
276 + cellText=rows,
277 + colLabels=["Region", "Index", "1m", "3m", "6m", "YoY", "Vs peak", "Sales 12m", "Grade"],
278 + loc="upper center", cellLoc="center", colWidths=widths)
279 + table.auto_set_font_size(False)
280 + table.set_fontsize(7.6)
281 + table.scale(1, 1.45)
282 + for (r, c), cell in table.get_celld().items():
283 + cell.set_edgecolor(BORDER)
284 + if c == 0:
285 + cell.set_text_props(ha="left")
286 + cell.PAD = 0.02
287 + if r == 0:
288 + cell.set_text_props(color=INK2, fontweight="bold",
289 + ha="left" if c == 0 else "center")
290 + cell.set_facecolor("#f0efec")
291 + elif c == 5:
292 + cell.set_facecolor(colors[r - 1])
293 + if colors[r - 1] in ("#256abf", "#3987e5"):
294 + cell.set_text_props(color="white")
295 + fig.text(0.05, 0.415, "YoY column shaded by appreciation (sequential blue ramp).",
296 + fontsize=7.5, color=MUTED)
297 +
298 + # YoY horizontal bars under the table
299 + axb = fig.add_axes([0.30, 0.075, 0.62, 0.30])
300 + names = [r["geography_name"] for r in ov["rows"]][::-1]
301 + yoys = [r["yoy_pct"] or 0 for r in ov["rows"]][::-1]
302 + axb.barh(np.arange(len(names)), yoys,
303 + color=[heat(v) for v in yoys], height=0.72)
304 + axb.set_yticks(np.arange(len(names)), names, fontsize=7)
305 + axb.set_title("Year-over-year appreciation (%)", fontsize=9,
306 + loc="left", color=INK2)
307 + axb.tick_params(axis="x", labelsize=7)
308 + _footer(fig, "regions")
309 + pdf.savefig(fig)
310 + plt.close(fig)
311 +
312 + # Page 3 — property types (province)
313 + fig = plt.figure(figsize=PAGE)
314 + _title_block(fig, "Property types — province",
315 + "Unifamilial, condo and plex indexes (2021 = 100)")
316 + ax = fig.add_axes([0.07, 0.52, 0.87, 0.36])
317 + for i, t in enumerate(["unifamilial", "condo", "plex"]):
318 + f = data.series("quebec", t).filter(~pl.col("is_partial_month")).sort("period")
319 + x = np.arange(f.height)
320 + ax.plot(x, f["index_smoothed"].to_numpy(), color=SERIES_COLORS[i],
321 + lw=1.9, label=t)
322 + periods = f["period"].to_list()
323 + step = max(1, len(periods) // 8)
324 + ax.set_xticks(np.arange(len(periods))[::step], periods[::step], fontsize=7.5)
325 + ax.legend(loc="upper left", fontsize=8.5, frameon=False)
326 +
327 + fig.text(0.07, 0.42, "Method", fontsize=11, fontweight="bold")
328 + fig.text(0.07, 0.20,
329 + "Robust monthly architecture: province paths estimated by rolling-time-dummy hedonic\n"
330 + "regressions (13-month Huber-weighted windows, mean-splice linking — the published\n"
331 + "history never revises); liquid cells (≥40 transactions/month) estimated directly by a\n"
332 + "local robust time-dummy; thin cells shrunk toward their parent path by a heteroskedastic\n"
333 + "local-level Kalman filter. Validated against repeat sales, stratified matched-cell\n"
334 + "medians, downsampling and composition-shock simulations. Variables with time-correlated\n"
335 + "missingness are excluded by rule. All series NSA, base 2021 average = 100.\n\n"
336 + "Full methodology: www.indexqc.house/methodology",
337 + fontsize=8.5, color=INK2, va="bottom")
338 + _footer(fig, "types & method")
339 + pdf.savefig(fig)
340 + plt.close(fig)
341 +
342 + meta = pdf.infodict()
343 + meta["Title"] = f"QHPI market report — {ptype}"
344 + meta["Author"] = "Simon-Pierre Boucher <contact@spboucher.ai>"
345 + return buf.getvalue()
added api/app/services/stats.py +164 −0
@@ -0,0 +1,164 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/app/services/stats.py
6 +# Purpose : Rich derived metrics per series — peak/drawdown, momentum,
7 +# volatility, CAGR, ranks, volumes, assessment gap.
8 +# =============================================================================
9 +"""Derived series statistics.
10 +
11 +Everything is computed from the canonical monthly parquet (complete months
12 +only). Definitions:
13 +
14 +- peak / drawdown ....... max of index_smoothed; % below it now
15 +- since_2021_pct ........ latest index − 100 (base = 2021 average)
16 +- cagr_pct .............. annualized log growth from the first 12-month mean
17 + to the latest 12-month mean
18 +- volatility_12m_pct .... SD of the last 12 monthly log changes (monthly, %)
19 +- momentum_3m_ann_pct ... last 3-month log growth, annualized
20 +- yoy_rank .............. rank of YoY among geographies of the same level
21 + and property type (1 = fastest)
22 +- volume_12m / volume_prev_12m / volume_yoy_pct
23 +- dollar_volume_12m ..... Σ transactions × representative value (estimate)
24 +- assessment_gap ........ latest median sale/assessment ratio (region &
25 + province only)
26 +"""
27 +
28 +from __future__ import annotations
29 +
30 +import math
31 +
32 +import numpy as np
33 +import polars as pl
34 +
35 +from app.services import data
36 +
37 +
38 +def _complete(gid: str, ptype: str) -> pl.DataFrame:
39 + return data.series(gid, ptype).filter(~pl.col("is_partial_month"))
40 +
41 +
42 +def series_stats(gid: str, ptype: str) -> dict:
43 + df = _complete(gid, ptype)
44 + if df.height < 14:
45 + return {}
46 + idx = df["index_smoothed"].to_numpy()
47 + periods = df["period"].to_list()
48 + li = np.log(idx)
49 +
50 + peak_i = int(np.argmax(idx))
51 + latest = float(idx[-1])
52 + peak = float(idx[peak_i])
53 + drawdown = (latest / peak - 1) * 100
54 + months_since_peak = len(idx) - 1 - peak_i
55 +
56 + first12 = float(np.mean(li[:12]))
57 + last12 = float(np.mean(li[-12:]))
58 + years = (len(li) - 12) / 12
59 + cagr = (math.exp((last12 - first12) / max(years, 1e-9)) - 1) * 100
60 +
61 + d = np.diff(li)
62 + vol12 = float(np.std(d[-12:])) * 100
63 + mom3 = (math.exp((li[-1] - li[-4]) * 4) - 1) * 100
64 +
65 + tx = df["transactions"].to_numpy().astype(float)
66 + rep = df["representative_value"].to_numpy()
67 + vol_12m = float(np.nansum(tx[-12:]))
68 + vol_prev = float(np.nansum(tx[-24:-12])) if len(tx) >= 24 else None
69 + volume_yoy = ((vol_12m / vol_prev - 1) * 100) if vol_prev else None
70 + dollar_12m = float(np.nansum(tx[-12:] * np.nan_to_num(rep[-12:])))
71 +
72 + last = df.row(df.height - 1, named=True)
73 +
74 + # Rank among same level × type on YoY.
75 + level = last["geography_level"]
76 + peers = (
77 + data.canonical()
78 + .filter((pl.col("geography_level") == level)
79 + & (pl.col("property_type") == ptype)
80 + & (pl.col("period") == last["period"]))
81 + .sort("yoy_pct", descending=True, nulls_last=True)
82 + )
83 + ids = peers["geography_id"].to_list()
84 + rank = (ids.index(gid) + 1) if gid in ids else None
85 +
86 + # Assessment gap (province & regions).
87 + gap = None
88 + gap_df = data.assessment_gap().filter(
89 + (pl.col("geography_id") == gid) & (pl.col("property_type") == ptype))
90 + if gap_df.height:
91 + gap = float(gap_df.sort("period")["median_ratio"][-1])
92 +
93 + hist12 = df.tail(24)
94 + return {
95 + "period": last["period"],
96 + "index": round(latest, 2),
97 + "representative_value": last["representative_value"],
98 + "lower_95": round(last["lower_95"], 2),
99 + "upper_95": round(last["upper_95"], 2),
100 + "reliability_grade": last["reliability_grade"],
101 + "monthly_pct": last["monthly_pct"],
102 + "three_month_pct": last["three_month_pct"],
103 + "six_month_pct": last["six_month_pct"],
104 + "yoy_pct": last["yoy_pct"],
105 + "since_2021_pct": round(latest - 100, 2),
106 + "cagr_pct": round(cagr, 2),
107 + "peak_index": round(peak, 2),
108 + "peak_period": periods[peak_i],
109 + "drawdown_pct": round(drawdown, 2),
110 + "months_since_peak": months_since_peak,
111 + "at_record_high": bool(months_since_peak == 0),
112 + "volatility_12m_pct": round(vol12, 2),
113 + "momentum_3m_ann_pct": round(mom3, 2),
114 + "yoy_rank": rank,
115 + "yoy_rank_of": len(ids),
116 + "volume_12m": int(vol_12m),
117 + "volume_yoy_pct": round(volume_yoy, 2) if volume_yoy is not None else None,
118 + "dollar_volume_12m": int(dollar_12m),
119 + "assessment_gap": gap,
120 + "effective_sample_size": last["effective_sample_size"],
121 + "spark": [round(v, 2) for v in hist12["index_smoothed"].to_list()],
122 + }
123 +
124 +
125 +def overview(level: str = "region", ptype: str = "all") -> list[dict]:
126 + """One rich row per geography of a level — powers the heatmap table."""
127 + c = data.canonical().filter(
128 + (pl.col("geography_level") == level)
129 + & (pl.col("property_type") == ptype)
130 + & (~pl.col("is_partial_month")))
131 + latest_period = c["period"].max()
132 + out = []
133 + for gid in c["geography_id"].unique().sort().to_list():
134 + df = c.filter(pl.col("geography_id") == gid).sort("period")
135 + if df.height < 14:
136 + continue
137 + idx = df["index_smoothed"].to_numpy()
138 + peak = float(np.max(idx))
139 + last = df.row(df.height - 1, named=True)
140 + tx = df["transactions"].to_numpy().astype(float)
141 + out.append({
142 + "geography_id": gid,
143 + "geography_name": last["geography_name"],
144 + "period": last["period"],
145 + "index": round(float(idx[-1]), 1),
146 + "monthly_pct": last["monthly_pct"],
147 + "three_month_pct": last["three_month_pct"],
148 + "six_month_pct": last["six_month_pct"],
149 + "yoy_pct": last["yoy_pct"],
150 + "since_2021_pct": round(float(idx[-1]) - 100, 1),
151 + "drawdown_pct": round((float(idx[-1]) / peak - 1) * 100, 2),
152 + "at_record_high": bool(np.argmax(idx) == len(idx) - 1),
153 + "volume_12m": int(np.nansum(tx[-12:])),
154 + "representative_value": last["representative_value"],
155 + "reliability_grade": last["reliability_grade"],
156 + "spark": [round(v, 2) for v in df["index_smoothed"].tail(24).to_list()],
157 + })
158 + return sorted(out, key=lambda r: -(r["yoy_pct"] or -999)), latest_period
159 +
160 +
161 +def overview_rows(level: str = "region", ptype: str = "all") -> dict:
162 + rows, latest_period = overview(level, ptype)
163 + return {"level": level, "property_type": ptype,
164 + "period": latest_period, "rows": rows}
added api/pyproject.toml +31 −0
@@ -0,0 +1,31 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/pyproject.toml
6 +# Purpose : Package metadata and dependencies for the QWHPI API service.
7 +# =============================================================================
8 +
9 +[project]
10 +name = "qwhpi-api"
11 +version = "0.1.0"
12 +description = "Quebec Weekly Housing Price Index — API service"
13 +authors = [{ name = "Simon-Pierre Boucher", email = "contact@spboucher.ai" }]
14 +requires-python = ">=3.12"
15 +dependencies = [
16 + "fastapi>=0.110",
17 + "uvicorn>=0.29",
18 + "polars>=1.0",
19 + "pydantic>=2.7",
20 + "psycopg[binary]>=3.1; extra == 'postgres'",
21 +]
22 +
23 +[project.optional-dependencies]
24 +dev = ["pytest>=8", "httpx>=0.27"]
25 +
26 +[build-system]
27 +requires = ["hatchling"]
28 +build-backend = "hatchling.build"
29 +
30 +[tool.hatch.build.targets.wheel]
31 +packages = ["app"]
added api/tests/test_api.py +136 −0
@@ -0,0 +1,136 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : api/tests/test_api.py
6 +# Purpose : API tests (monthly v2.1) — every router, ETag / CSV / pagination.
7 +# =============================================================================
8 +"""API test suite (runs against the real canonical monthly parquet)."""
9 +
10 +from __future__ import annotations
11 +
12 +import sys
13 +from pathlib import Path
14 +
15 +sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
16 +
17 +from fastapi.testclient import TestClient
18 +
19 +from app.main import app
20 +
21 +client = TestClient(app)
22 +
23 +
24 +def test_health():
25 + r = client.get("/health")
26 + assert r.status_code == 200
27 + assert r.json()["status"] == "ok"
28 +
29 +
30 +def test_meta_credits_author_and_monthly():
31 + r = client.get("/v1/meta")
32 + assert r.status_code == 200
33 + body = r.json()
34 + assert body["author"] == "Simon-Pierre Boucher"
35 + assert body["contact"] == "contact@spboucher.ai"
36 + assert body["frequency"] == "monthly"
37 + assert body["series_count"] > 100
38 + assert body["periods"] >= 60
39 +
40 +
41 +def test_index_series_full():
42 + r = client.get("/v1/index", params={"geography": "quebec-city", "type": "condo"})
43 + assert r.status_code == 200
44 + body = r.json()
45 + assert body["frequency"] == "monthly"
46 + obs = body["observations"]
47 + assert len(obs) == body["total_observations"] >= 60
48 + last = obs[-1]
49 + for key in ("lower_95", "upper_95", "reliability_grade", "transactions",
50 + "monthly_pct", "yoy_pct"):
51 + assert key in last # uncertainty is never hidden
52 +
53 +
54 +def test_index_pagination():
55 + r = client.get("/v1/index", params={"geography": "quebec", "type": "all",
56 + "limit": 10, "offset": 5})
57 + body = r.json()
58 + assert len(body["observations"]) == 10
59 + assert body["offset"] == 5
60 +
61 +
62 +def test_index_csv_export():
63 + r = client.get("/v1/index", params={"geography": "montreal", "type": "condo",
64 + "format": "csv"})
65 + assert r.status_code == 200
66 + assert r.headers["content-type"].startswith("text/csv")
67 + assert r.text.splitlines()[0].startswith("period")
68 +
69 +
70 +def test_index_etag_304():
71 + r1 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"})
72 + etag = r1.headers.get("etag")
73 + assert etag
74 + r2 = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"},
75 + headers={"If-None-Match": etag})
76 + assert r2.status_code == 304
77 +
78 +
79 +def test_latest_excludes_partial_month():
80 + r = client.get("/v1/index/latest", params={"geography": "quebec", "type": "all"})
81 + body = r.json()
82 + assert body["is_partial_month"] is False
83 + assert body["reliability"] in "ABCDE"
84 + assert body["period"] >= "2026-06"
85 +
86 +
87 +def test_unknown_geography_404():
88 + r = client.get("/v1/index", params={"geography": "atlantis", "type": "all"})
89 + assert r.status_code == 404
90 +
91 +
92 +def test_bad_type_422():
93 + r = client.get("/v1/index", params={"geography": "quebec", "type": "castle"})
94 + assert r.status_code == 422
95 +
96 +
97 +def test_geographies_hierarchy():
98 + r = client.get("/v1/geographies")
99 + body = r.json()
100 + levels = {g["geography_level"] for g in body["published_series"]}
101 + assert levels == {"province", "region", "municipality"}
102 +
103 +
104 +def test_coverage_matrix():
105 + r = client.get("/v1/geographies/coverage", params={"status": "published"})
106 + assert r.status_code == 200
107 + assert len(r.json()["coverage"]) > 0
108 +
109 +
110 +def test_liquidity():
111 + r = client.get("/v1/liquidity", params={"geography": "montreal", "type": "condo"})
112 + body = r.json()
113 + assert body["median_monthly_transactions"] > 400
114 +
115 +
116 +def test_compare_with_rebase():
117 + r = client.get("/v1/compare", params={
118 + "series": "montreal:condo,quebec-city:condo",
119 + "rebase_period": "2022-01"})
120 + body = r.json()
121 + assert set(body["series"]) == {"montreal:condo", "quebec-city:condo"}
122 + for obs in body["series"].values():
123 + first = [o for o in obs if o["period"] == "2022-01"][0]
124 + assert abs(first["index_smoothed"] - 100.0) < 1e-6
125 +
126 +
127 +def test_map_region_yoy():
128 + r = client.get("/v1/map", params={"metric": "yoy", "level": "region"})
129 + body = r.json()
130 + assert len(body["cells"]) == 17
131 +
132 +
133 +def test_vintages():
134 + r = client.get("/v1/vintages", params={"geography": "quebec", "type": "all"})
135 + assert r.status_code == 200
136 + assert len(r.json()["observations"]) >= 60
added data/external/SDA_SOURCE.md +32 −0
@@ -0,0 +1,32 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : data/external/SDA_SOURCE.md
7 +Purpose : Provenance record for the authoritative boundary dataset.
8 +=============================================================================
9 +-->
10 +
11 +# Boundary dataset provenance — SDA 1/20 000
12 +
13 +- **Dataset**: Découpages administratifs à l'échelle 1/20 000 (SDA)
14 +- **Producer**: Gouvernement du Québec — Ministère des Ressources naturelles
15 + et des Forêts (MRNF), Registre du domaine de l'État
16 +- **Catalog page**: https://www.donneesquebec.ca/recherche/dataset/decoupages-administratifs
17 +- **File downloaded**: `SDA.gpkg.zip` (GPKG),
18 + https://diffusion.mern.gouv.qc.ca/diffusion/RGQ/Vectoriel/Theme/Local/SDA_20k/GPKG/SDA.gpkg.zip
19 +- **Download date**: 2026-08-06
20 +- **Version**: V2026-07 (layer attribute `*_CO_VER`)
21 +- **Native CRS**: EPSG:4269 (NAD83 geographic); metric operations performed in
22 + EPSG:32198 (NAD83 / Quebec Lambert)
23 +- **Layers used**:
24 + - `munic_s` — municipal polygons (geocode, name, MRC, administrative region)
25 + - `regio_s` — 17 administrative regions (map rendering)
26 + - `mrc_s` — MRCs (optional intermediate level)
27 +- **License**: CC-BY 4.0 (Données Québec)
28 +
29 +Join result persisted at `data/processed/geo_join.parquet`
30 +(one row per transaction id; join_method ∈ {within, nearest<=1km, unassigned}).
31 +Validation: 100.000% `within`, 99.92% name agreement with the raw `city` field
32 +(see `outputs/reports/geography_report.md`).
added data/processed/assessment_gap.parquet +0 −0

Binary file not shown.

added data/processed/assessment_gap_monthly.parquet +0 −0

Binary file not shown.

added data/processed/coverage_matrix.parquet +0 −0

Binary file not shown.

added data/processed/coverage_matrix_monthly.parquet +0 −0

Binary file not shown.

added data/processed/first_release.parquet +0 −0

Binary file not shown.

added data/processed/first_release_monthly.parquet +0 −0

Binary file not shown.

added data/processed/monthly_liquidity.parquet +0 −0

Binary file not shown.

added data/processed/municipalities.parquet +0 −0

Binary file not shown.

added data/processed/qhpi_monthly.parquet +0 −0

Binary file not shown.

added data/processed/qwhpi_weekly.parquet +0 −0

Binary file not shown.

added data/processed/vintages/data_vintage=2026-08-06/qwhpi_weekly.parquet +0 −0

Binary file not shown.

added data/processed/vintages_monthly/data_vintage=2026-08-08/qhpi_monthly.parquet +0 −0

Binary file not shown.

added data/processed/weekly_liquidity.parquet +0 −0

Binary file not shown.

added db/alembic.ini +32 −0
@@ -0,0 +1,32 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : db/alembic.ini
6 +# Purpose : Alembic configuration for QWHPI database migrations.
7 +# =============================================================================
8 +[alembic]
9 +script_location = migrations
10 +sqlalchemy.url = ${DATABASE_URL}
11 +
12 +[loggers]
13 +keys = root
14 +
15 +[handlers]
16 +keys = console
17 +
18 +[formatters]
19 +keys = generic
20 +
21 +[logger_root]
22 +level = WARN
23 +handlers = console
24 +
25 +[handler_console]
26 +class = StreamHandler
27 +args = (sys.stderr,)
28 +level = NOTSET
29 +formatter = generic
30 +
31 +[formatter_generic]
32 +format = %(levelname)-5.5s [%(name)s] %(message)s
added db/migrations/env.py +37 −0
@@ -0,0 +1,37 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : db/migrations/env.py
6 +# Purpose : Alembic environment — runs SQL migrations against DATABASE_URL.
7 +# =============================================================================
8 +"""Alembic migration environment (offline + online modes)."""
9 +
10 +from __future__ import annotations
11 +
12 +import os
13 +
14 +from alembic import context
15 +from sqlalchemy import create_engine
16 +
17 +url = os.environ.get("DATABASE_URL", "postgresql://qwhpi:qwhpi@localhost:5432/qwhpi")
18 +
19 +
20 +def run_migrations_offline() -> None:
21 + context.configure(url=url, literal_binds=True)
22 + with context.begin_transaction():
23 + context.run_migrations()
24 +
25 +
26 +def run_migrations_online() -> None:
27 + engine = create_engine(url)
28 + with engine.connect() as connection:
29 + context.configure(connection=connection)
30 + with context.begin_transaction():
31 + context.run_migrations()
32 +
33 +
34 +if context.is_offline_mode():
35 + run_migrations_offline()
36 +else:
37 + run_migrations_online()
added db/migrations/versions/001_initial_schema.py +35 −0
@@ -0,0 +1,35 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : db/migrations/versions/001_initial_schema.py
6 +# Purpose : Initial migration — creates the full QWHPI schema (db/schema.sql).
7 +# =============================================================================
8 +"""Initial QWHPI schema.
9 +
10 +Revision ID: 001
11 +Revises:
12 +"""
13 +
14 +from __future__ import annotations
15 +
16 +from pathlib import Path
17 +
18 +from alembic import op
19 +
20 +revision = "001"
21 +down_revision = None
22 +branch_labels = None
23 +depends_on = None
24 +
25 +SCHEMA_SQL = Path(__file__).resolve().parents[2] / "schema.sql"
26 +
27 +
28 +def upgrade() -> None:
29 + op.execute(SCHEMA_SQL.read_text())
30 +
31 +
32 +def downgrade() -> None:
33 + for table in ("vintages", "liquidity", "observations", "model_runs",
34 + "series", "geographies"):
35 + op.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
added db/schema.sql +74 −0
@@ -0,0 +1,74 @@
1 +-- =============================================================================
2 +-- QWHPI — Quebec Weekly Housing Price Index
3 +-- Author : Simon-Pierre Boucher
4 +-- Contact : contact@spboucher.ai
5 +-- File : db/schema.sql
6 +-- Purpose : Relational schema serving the API (loaded by engine export.py).
7 +-- =============================================================================
8 +
9 +CREATE TABLE IF NOT EXISTS geographies (
10 + geography_id TEXT PRIMARY KEY,
11 + geography_name TEXT NOT NULL,
12 + geography_level TEXT NOT NULL CHECK (geography_level IN ('province','region','municipality')),
13 + parent_id TEXT REFERENCES geographies(geography_id)
14 +);
15 +
16 +CREATE TABLE IF NOT EXISTS series (
17 + series_id TEXT PRIMARY KEY, -- '<geography_id>:<property_type>'
18 + geography_id TEXT NOT NULL REFERENCES geographies(geography_id),
19 + property_type TEXT NOT NULL CHECK (property_type IN ('all','unifamilial','condo','plex')),
20 + base_period TEXT NOT NULL DEFAULT '2021 average = 100',
21 + UNIQUE (geography_id, property_type)
22 +);
23 +
24 +CREATE TABLE IF NOT EXISTS model_runs (
25 + run_id SERIAL PRIMARY KEY,
26 + model_version TEXT NOT NULL,
27 + data_vintage DATE NOT NULL,
28 + input_row_count BIGINT,
29 + input_hash TEXT,
30 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
31 + UNIQUE (model_version, data_vintage)
32 +);
33 +
34 +CREATE TABLE IF NOT EXISTS observations (
35 + series_id TEXT NOT NULL REFERENCES series(series_id),
36 + week DATE NOT NULL,
37 + index DOUBLE PRECISION,
38 + index_smoothed DOUBLE PRECISION NOT NULL,
39 + representative_value DOUBLE PRECISION,
40 + transactions INTEGER NOT NULL,
41 + effective_sample_size DOUBLE PRECISION,
42 + weekly_pct DOUBLE PRECISION,
43 + four_week_pct DOUBLE PRECISION,
44 + thirteen_week_pct DOUBLE PRECISION,
45 + twenty_six_week_pct DOUBLE PRECISION,
46 + yoy_pct DOUBLE PRECISION,
47 + lower_95 DOUBLE PRECISION NOT NULL,
48 + upper_95 DOUBLE PRECISION NOT NULL,
49 + reliability_grade CHAR(1) NOT NULL CHECK (reliability_grade IN ('A','B','C','D','E')),
50 + shrinkage_weight DOUBLE PRECISION,
51 + is_partial_week BOOLEAN NOT NULL DEFAULT FALSE,
52 + model_version TEXT NOT NULL,
53 + data_vintage DATE NOT NULL,
54 + PRIMARY KEY (series_id, week)
55 +);
56 +CREATE INDEX IF NOT EXISTS idx_obs_week ON observations (week);
57 +
58 +CREATE TABLE IF NOT EXISTS liquidity (
59 + series_id TEXT NOT NULL REFERENCES series(series_id),
60 + week DATE NOT NULL,
61 + transactions INTEGER NOT NULL,
62 + PRIMARY KEY (series_id, week)
63 +);
64 +
65 +CREATE TABLE IF NOT EXISTS vintages (
66 + series_id TEXT NOT NULL REFERENCES series(series_id),
67 + week DATE NOT NULL,
68 + first_release_index DOUBLE PRECISION,
69 + first_release_index_smoothed DOUBLE PRECISION NOT NULL,
70 + first_release_vintage DATE NOT NULL,
71 + PRIMARY KEY (series_id, week)
72 +);
73 +
74 +-- Author credit: Simon-Pierre Boucher — contact@spboucher.ai
added docker-compose.yml +56 −0
@@ -0,0 +1,56 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : docker-compose.yml
6 +# Purpose : Full platform — Postgres, API, dashboard, weekly scheduler.
7 +# =============================================================================
8 +
9 +services:
10 + db:
11 + image: postgres:16-alpine
12 + environment:
13 + POSTGRES_USER: qwhpi
14 + POSTGRES_PASSWORD: qwhpi
15 + POSTGRES_DB: qwhpi
16 + volumes:
17 + - pgdata:/var/lib/postgresql/data
18 + healthcheck:
19 + test: ["CMD-SHELL", "pg_isready -U qwhpi"]
20 + interval: 5s
21 + retries: 10
22 +
23 + api:
24 + build: ./api
25 + environment:
26 + QWHPI_DATA_DIR: /data/processed
27 + DATABASE_URL: postgresql://qwhpi:qwhpi@db:5432/qwhpi
28 + volumes:
29 + - ./data/processed:/data/processed:ro
30 + ports:
31 + - "8080:8080"
32 + depends_on:
33 + db:
34 + condition: service_healthy
35 +
36 + web:
37 + build: ./web
38 + environment:
39 + NEXT_PUBLIC_API_URL: http://localhost:8080
40 + ports:
41 + - "3000:3000"
42 + depends_on:
43 + - api
44 +
45 + scheduler:
46 + build: ./ops/scheduler
47 + environment:
48 + DATABASE_URL: postgresql://qwhpi:qwhpi@db:5432/qwhpi
49 + volumes:
50 + - ./:/workspace
51 + depends_on:
52 + db:
53 + condition: service_healthy
54 +
55 +volumes:
56 + pgdata:
added engine/pyproject.toml +43 −0
@@ -0,0 +1,43 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/pyproject.toml
6 +# Purpose : Package metadata and dependencies for the QWHPI index engine.
7 +# =============================================================================
8 +
9 +[project]
10 +name = "qwhpi"
11 +version = "0.1.0"
12 +description = "Quebec Weekly Housing Price Index — index engine"
13 +authors = [{ name = "Simon-Pierre Boucher", email = "contact@spboucher.ai" }]
14 +requires-python = ">=3.12"
15 +dependencies = [
16 + "pandas>=2.2",
17 + "polars>=1.0",
18 + "numpy>=1.26",
19 + "pyarrow>=16",
20 + "statsmodels>=0.14",
21 + "scikit-learn>=1.4",
22 + "geopandas>=1.0",
23 + "shapely>=2.0",
24 + "scipy>=1.13",
25 + "matplotlib>=3.8",
26 +]
27 +
28 +[project.optional-dependencies]
29 +dev = ["pytest>=8", "mypy>=1.10", "ruff>=0.4"]
30 +
31 +[build-system]
32 +requires = ["hatchling"]
33 +build-backend = "hatchling.build"
34 +
35 +[tool.hatch.build.targets.wheel]
36 +packages = ["src/qwhpi"]
37 +
38 +[tool.ruff]
39 +line-length = 100
40 +
41 +[tool.mypy]
42 +python_version = "3.12"
43 +ignore_missing_imports = true
added engine/scripts/01_profile.py +301 −0
@@ -0,0 +1,301 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/01_profile.py
7 +# Purpose : Step 1 audit — schema, descriptives, missingness, outlier scan,
8 +# coverage, duplicates, and weekly liquidity matrices. No index yet.
9 +# =============================================================================
10 +"""Full dataset audit (Execution Order step 1).
11 +
12 +Idempotent: re-running overwrites its own outputs. Writes tables to
13 +``outputs/tables/`` and a human-readable report to
14 +``outputs/reports/audit_report.md``. The raw CSV is never modified.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import sys
20 +from pathlib import Path
21 +
22 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
23 +
24 +import polars as pl
25 +
26 +from qwhpi.config import REPORTS_DIR, TABLES_DIR, ensure_dirs
27 +from qwhpi.ingest import load_raw
28 +
29 +pl.Config.set_tbl_rows(50)
30 +
31 +REPORT_LINES: list[str] = []
32 +
33 +
34 +def note(line: str = "") -> None:
35 + REPORT_LINES.append(line)
36 + print(line)
37 +
38 +
39 +def save(df: pl.DataFrame, name: str) -> None:
40 + path = TABLES_DIR / f"{name}.csv"
41 + df.write_csv(path)
42 + note(f" -> table saved: outputs/tables/{name}.csv ({df.height} rows)")
43 +
44 +
45 +def main() -> None:
46 + ensure_dirs()
47 + df = load_raw()
48 +
49 + note("# QWHPI Data Audit Report")
50 + note("")
51 + note("Author: Simon-Pierre Boucher — contact@spboucher.ai")
52 + note("")
53 +
54 + # ------------------------------------------------------------------ #
55 + # 1. Schema & basic shape
56 + # ------------------------------------------------------------------ #
57 + note("## 1. Schema and shape")
58 + note("")
59 + note(f"- Rows: {df.height:,}")
60 + note(f"- Columns: {df.width}")
61 + note(f"- Date range: {df['date'].min()} -> {df['date'].max()}")
62 + n_weeks = df["week"].n_unique()
63 + note(f"- Distinct Monday-labeled weeks with >=1 transaction: {n_weeks}")
64 + note(f"- Distinct cities (raw text field): {df['city'].n_unique():,}")
65 + note(f"- Distinct ids: {df['id'].n_unique():,} (duplicated ids: {df.height - df['id'].n_unique():,})")
66 + note("")
67 + schema_tbl = pl.DataFrame(
68 + {"column": df.columns, "dtype": [str(t) for t in df.dtypes]}
69 + )
70 + save(schema_tbl, "audit_schema")
71 +
72 + # ------------------------------------------------------------------ #
73 + # 2. Missingness
74 + # ------------------------------------------------------------------ #
75 + note("")
76 + note("## 2. Missingness")
77 + note("")
78 + nulls = df.null_count().transpose(include_header=True,
79 + header_name="column",
80 + column_names=["n_missing"])
81 + nulls = nulls.with_columns(
82 + (pl.col("n_missing") / df.height * 100).round(2).alias("pct_missing")
83 + ).sort("n_missing", descending=True)
84 + save(nulls, "audit_missingness")
85 + for row in nulls.filter(pl.col("n_missing") > 0).iter_rows(named=True):
86 + note(f"- `{row['column']}`: {row['n_missing']:,} missing ({row['pct_missing']}%)")
87 +
88 + # Sentinel-style missingness that null counts miss.
89 + note("")
90 + note("Sentinel / degenerate values (not encoded as null):")
91 + sentinels = pl.DataFrame({
92 + "check": [
93 + "amount <= 0", "amount in (1, 2)", "amount < 5000",
94 + "floorArea <= 0", "floorArea > 2000 m2",
95 + "yearBuilt < 1600", "yearBuilt > 2026", "yearBuilt == 0",
96 + "previousValue <= 0", "totalArValue <= 0",
97 + "lat/lng outside Quebec bbox (44.9-62.7N, -79.8--57W)",
98 + ],
99 + "n": [
100 + df.filter(pl.col("amount") <= 0).height,
101 + df.filter(pl.col("amount").is_in([1, 2])).height,
102 + df.filter(pl.col("amount") < 5000).height,
103 + df.filter(pl.col("floorArea") <= 0).height,
104 + df.filter(pl.col("floorArea") > 2000).height,
105 + df.filter(pl.col("yearBuilt") < 1600).height,
106 + df.filter(pl.col("yearBuilt") > 2026).height,
107 + df.filter(pl.col("yearBuilt") == 0).height,
108 + df.filter(pl.col("previousValue") <= 0).height,
109 + df.filter(pl.col("totalArValue") <= 0).height,
110 + df.filter(
111 + (pl.col("lat") < 44.9) | (pl.col("lat") > 62.7)
112 + | (pl.col("lng") < -79.8) | (pl.col("lng") > -57.0)
113 + ).height,
114 + ],
115 + })
116 + save(sentinels, "audit_sentinel_values")
117 + for row in sentinels.iter_rows(named=True):
118 + note(f"- {row['check']}: {row['n']:,}")
119 +
120 + # ------------------------------------------------------------------ #
121 + # 3. Categorical distributions
122 + # ------------------------------------------------------------------ #
123 + note("")
124 + note("## 3. Categorical distributions")
125 + for col in ("propertyType", "buildingType", "ownerType"):
126 + note("")
127 + note(f"### {col}")
128 + dist = (
129 + df.group_by(col).len().sort("len", descending=True)
130 + .with_columns((pl.col("len") / df.height * 100).round(2).alias("pct"))
131 + )
132 + save(dist, f"audit_dist_{col}")
133 + for row in dist.head(15).iter_rows(named=True):
134 + note(f"- {row[col]}: {row['len']:,} ({row['pct']}%)")
135 +
136 + # ------------------------------------------------------------------ #
137 + # 4. Price descriptives (by property type)
138 + # ------------------------------------------------------------------ #
139 + note("")
140 + note("## 4. Amount descriptives by propertyType (amount > 0)")
141 + note("")
142 + pos = df.filter(pl.col("amount") > 0)
143 + desc = (
144 + pos.group_by("propertyType")
145 + .agg(
146 + n=pl.len(),
147 + p01=pl.col("amount").quantile(0.01),
148 + p05=pl.col("amount").quantile(0.05),
149 + p25=pl.col("amount").quantile(0.25),
150 + median=pl.col("amount").median(),
151 + p75=pl.col("amount").quantile(0.75),
152 + p95=pl.col("amount").quantile(0.95),
153 + p99=pl.col("amount").quantile(0.99),
154 + max=pl.col("amount").max(),
155 + mean=pl.col("amount").mean().round(0),
156 + )
157 + .sort("n", descending=True)
158 + )
159 + save(desc, "audit_amount_by_type")
160 + for row in desc.iter_rows(named=True):
161 + note(
162 + f"- {row['propertyType']}: n={row['n']:,} | p01=${row['p01']:,.0f} "
163 + f"| median=${row['median']:,.0f} | p99=${row['p99']:,.0f} | max=${row['max']:,.0f}"
164 + )
165 +
166 + # Same for hedonic characteristics.
167 + char_desc = (
168 + df.group_by("propertyType")
169 + .agg(
170 + n=pl.len(),
171 + floorArea_med=pl.col("floorArea").median(),
172 + floorArea_p99=pl.col("floorArea").quantile(0.99),
173 + yearBuilt_med=pl.col("yearBuilt").median(),
174 + yearBuilt_min=pl.col("yearBuilt").min(),
175 + floorArea_miss_pct=(pl.col("floorArea").is_null().mean() * 100).round(2),
176 + yearBuilt_miss_pct=(pl.col("yearBuilt").is_null().mean() * 100).round(2),
177 + buildingType_miss_pct=(pl.col("buildingType").is_null().mean() * 100).round(2),
178 + )
179 + .sort("n", descending=True)
180 + )
181 + save(char_desc, "audit_characteristics_by_type")
182 +
183 + # ------------------------------------------------------------------ #
184 + # 5. Duplicates
185 + # ------------------------------------------------------------------ #
186 + note("")
187 + note("## 5. Duplicate scan")
188 + note("")
189 + full_dupes = df.height - df.unique(subset=[c for c in df.columns if c not in ("id",)]).height
190 + addr_date = df.height - df.unique(subset=["street", "city", "date"]).height
191 + addr_date_amt = df.height - df.unique(subset=["street", "city", "date", "amount"]).height
192 + note(f"- Exact duplicates ignoring id: {full_dupes:,}")
193 + note(f"- Same street+city+date (candidate multi-unit or true dupes): {addr_date:,}")
194 + note(f"- Same street+city+date+amount (strong duplicate candidates): {addr_date_amt:,}")
195 +
196 + # Repeat-sales potential: same street+city appearing on different dates.
197 + repeat = (
198 + df.group_by(["street", "city"]).agg(n_dates=pl.col("date").n_unique())
199 + .filter(pl.col("n_dates") >= 2)
200 + )
201 + note(f"- Addresses (street+city) with >=2 distinct sale dates: {repeat.height:,}")
202 +
203 + # ------------------------------------------------------------------ #
204 + # 6. Weekly coverage & liquidity
205 + # ------------------------------------------------------------------ #
206 + note("")
207 + note("## 6. Weekly coverage and liquidity")
208 + note("")
209 + weekly = df.group_by("week").len().sort("week")
210 + first, last = weekly["week"].min(), weekly["week"].max()
211 + grid = pl.DataFrame({"week": pl.date_range(first, last, "1w", eager=True)})
212 + weekly_full = grid.join(weekly, on="week", how="left").fill_null(0)
213 + zero_weeks = weekly_full.filter(pl.col("len") == 0)
214 + note(f"- Continuous Monday grid: {grid.height} weeks ({first} -> {last})")
215 + note(f"- Zero-transaction weeks on the grid: {zero_weeks.height}")
216 + note(f"- Median tx/week (province): {weekly_full['len'].median():,.0f}")
217 + note(f"- Min tx/week: {weekly_full['len'].min():,} | Max: {weekly_full['len'].max():,}")
218 + tail = weekly_full.tail(6)
219 + note(f"- Last 6 weeks (partial-week check): "
220 + + ", ".join(f"{r['week']}={r['len']}" for r in tail.iter_rows(named=True)))
221 + save(weekly_full.rename({"len": "transactions"}), "audit_weekly_volume_province")
222 +
223 + # Liquidity matrix: median weekly tx for top cities x property type.
224 + top_cities = (
225 + df.group_by("city").len().sort("len", descending=True).head(25)["city"].to_list()
226 + )
227 + liq = (
228 + df.filter(pl.col("city").is_in(top_cities))
229 + .group_by(["city", "propertyType", "week"]).len()
230 + .group_by(["city", "propertyType"])
231 + .agg(
232 + median_wk=pl.col("len").median(),
233 + mean_wk=pl.col("len").mean().round(1),
234 + weeks_present=pl.len(),
235 + )
236 + .with_columns(
237 + pl.col("weeks_present").truediv(grid.height).mul(100).round(1).alias("pct_weeks_present")
238 + )
239 + .sort(["city", "propertyType"])
240 + )
241 + save(liq, "audit_weekly_liquidity_topcities")
242 +
243 + pivot = (
244 + liq.pivot(values="median_wk", index="city", on="propertyType")
245 + .sort("unifamilial", descending=True, nulls_last=True)
246 + )
247 + save(pivot, "audit_liquidity_matrix")
248 + note("")
249 + note("Median weekly transactions, top cities (rows) x type (cols): see audit_liquidity_matrix.csv")
250 + for row in pivot.head(12).iter_rows(named=True):
251 + note(f"- {row}")
252 +
253 + # City name hygiene: how many city strings, top mass share.
254 + city_dist = df.group_by("city").len().sort("len", descending=True)
255 + top50_share = city_dist.head(50)["len"].sum() / df.height * 100
256 + note("")
257 + note(f"- Top 50 city strings cover {top50_share:.1f}% of transactions")
258 + save(city_dist.head(200), "audit_city_top200")
259 +
260 + # ------------------------------------------------------------------ #
261 + # 7. indéterminé investigation (preliminary)
262 + # ------------------------------------------------------------------ #
263 + note("")
264 + note("## 7. `indéterminé` property type — preliminary look")
265 + note("")
266 + ind = df.filter(pl.col("propertyType") == "indéterminé")
267 + note(f"- Count: {ind.height:,} ({ind.height / df.height * 100:.1f}%)")
268 + if ind.height:
269 + note(f"- Median amount: ${ind['amount'].median():,.0f} "
270 + f"(vs unifamilial ${df.filter(pl.col('propertyType') == 'unifamilial')['amount'].median():,.0f})")
271 + note(f"- floorArea missing: {ind['floorArea'].is_null().mean() * 100:.1f}% | "
272 + f"yearBuilt missing: {ind['yearBuilt'].is_null().mean() * 100:.1f}% | "
273 + f"buildingType missing: {ind['buildingType'].is_null().mean() * 100:.1f}%")
274 + bt = ind.group_by("buildingType").len().sort("len", descending=True).head(8)
275 + note("- Top buildingType values within indéterminé: "
276 + + ", ".join(f"{r['buildingType']}={r['len']:,}" for r in bt.iter_rows(named=True)))
277 + ot = ind.group_by("ownerType").len().sort("len", descending=True).head(5)
278 + note("- ownerType within indéterminé: "
279 + + ", ".join(f"{r['ownerType']}={r['len']:,}" for r in ot.iter_rows(named=True)))
280 +
281 + # ------------------------------------------------------------------ #
282 + # Write report
283 + # ------------------------------------------------------------------ #
284 + report_path = REPORTS_DIR / "audit_report.md"
285 + header = (
286 + "<!--\n"
287 + "=============================================================================\n"
288 + "QWHPI — Quebec Weekly Housing Price Index\n"
289 + "Author : Simon-Pierre Boucher\n"
290 + "Contact : contact@spboucher.ai\n"
291 + "File : outputs/reports/audit_report.md\n"
292 + "Purpose : Step 1 data audit report (generated by engine/scripts/01_profile.py)\n"
293 + "=============================================================================\n"
294 + "-->\n\n"
295 + )
296 + report_path.write_text(header + "\n".join(REPORT_LINES) + "\n", encoding="utf-8")
297 + print(f"\nReport written to {report_path}")
298 +
299 +
300 +if __name__ == "__main__":
301 + main()
added engine/scripts/02_geography.py +132 −0
@@ -0,0 +1,132 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/02_geography.py
7 +# Purpose : Step 2 — spatial join of all transactions to official boundaries,
8 +# validation against the raw city text field, persistence.
9 +# =============================================================================
10 +"""Geography build (Execution Order step 2).
11 +
12 +Runs the spatial join (cached in ``data/processed/geo_join.parquet``),
13 +validates the result against the raw ``city`` text field, and writes
14 +validation tables + a report section.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import sys
20 +import time
21 +import unicodedata
22 +from pathlib import Path
23 +
24 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
25 +
26 +import polars as pl
27 +
28 +from qwhpi.config import REPORTS_DIR, TABLES_DIR, ensure_dirs
29 +from qwhpi.geography import SDA_VERSION, build_geo_join
30 +from qwhpi.ingest import load_raw
31 +
32 +LINES: list[str] = []
33 +
34 +
35 +def note(line: str = "") -> None:
36 + LINES.append(line)
37 + print(line)
38 +
39 +
40 +def norm_name(s: str) -> str:
41 + """Accent/case/punctuation-insensitive normalization for name comparison."""
42 + if s is None:
43 + return ""
44 + s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
45 + return (
46 + s.lower().replace("saint-", "st-").replace("sainte-", "ste-")
47 + .replace("'", "").replace("’", "").replace(".", "").replace(" ", "-")
48 + )
49 +
50 +
51 +def main() -> None:
52 + ensure_dirs()
53 + tx = load_raw()
54 +
55 + t0 = time.time()
56 + geo = build_geo_join(tx)
57 + note("## Geography spatial join (SDA 1/20k, version " + SDA_VERSION + ")")
58 + note("")
59 + note(f"- Join computed/loaded in {time.time() - t0:.1f}s for {geo.height:,} transactions")
60 +
61 + method_counts = geo.group_by("join_method").len().sort("len", descending=True)
62 + for row in method_counts.iter_rows(named=True):
63 + note(f"- join_method={row['join_method']}: {row['len']:,} "
64 + f"({row['len'] / geo.height * 100:.3f}%)")
65 +
66 + n_regions = geo["region"].n_unique()
67 + n_munic = geo["geo_code"].n_unique()
68 + note(f"- Distinct regions matched: {n_regions} | municipalities: {n_munic:,}")
69 +
70 + region_dist = (
71 + geo.group_by(["region_code", "region"]).len()
72 + .sort("len", descending=True)
73 + .rename({"len": "n_transactions"})
74 + )
75 + region_dist.write_csv(TABLES_DIR / "geo_region_distribution.csv")
76 + note("")
77 + note("Transactions by administrative region:")
78 + for row in region_dist.iter_rows(named=True):
79 + note(f"- {row['region_code']} {row['region']}: {row['n_transactions']:,}")
80 +
81 + # ---------------------------------------------------------------- #
82 + # Validation vs raw city text field
83 + # ---------------------------------------------------------------- #
84 + joined = tx.select("id", "city").join(geo, on="id", how="left")
85 + comparable = joined.filter(pl.col("municipality").is_not_null())
86 + comp = comparable.with_columns(
87 + pl.col("city").map_elements(norm_name, return_dtype=pl.Utf8).alias("city_norm"),
88 + pl.col("municipality").map_elements(norm_name, return_dtype=pl.Utf8).alias("mun_norm"),
89 + ).with_columns(
90 + (
91 + (pl.col("city_norm") == pl.col("mun_norm"))
92 + | pl.col("mun_norm").str.contains(pl.col("city_norm"), literal=True)
93 + | pl.col("city_norm").str.contains(pl.col("mun_norm"), literal=True)
94 + ).alias("name_match")
95 + )
96 + match_rate = comp["name_match"].mean() * 100
97 + note("")
98 + note("### Validation: joined municipality vs raw `city` text")
99 + note(f"- Name agreement (normalized, containment-tolerant): {match_rate:.2f}%")
100 +
101 + mismatches = (
102 + comp.filter(~pl.col("name_match"))
103 + .group_by(["city", "municipality", "region"]).len()
104 + .sort("len", descending=True)
105 + .rename({"len": "n"})
106 + )
107 + mismatches.head(300).write_csv(TABLES_DIR / "geo_city_mismatch_top300.csv")
108 + note(f"- Mismatching pairs saved: outputs/tables/geo_city_mismatch_top300.csv "
109 + f"({mismatches.height:,} distinct pairs, {mismatches['n'].sum():,} transactions)")
110 + note("")
111 + note("Top 15 mismatch pairs (city text -> joined municipality):")
112 + for row in mismatches.head(15).iter_rows(named=True):
113 + note(f"- '{row['city']}' -> '{row['municipality']}' ({row['region']}): {row['n']:,}")
114 +
115 + report_path = REPORTS_DIR / "geography_report.md"
116 + header = (
117 + "<!--\n"
118 + "=============================================================================\n"
119 + "QWHPI — Quebec Weekly Housing Price Index\n"
120 + "Author : Simon-Pierre Boucher\n"
121 + "Contact : contact@spboucher.ai\n"
122 + "File : outputs/reports/geography_report.md\n"
123 + "Purpose : Step 2 geography join + validation report (from 02_geography.py)\n"
124 + "=============================================================================\n"
125 + "-->\n\n"
126 + )
127 + report_path.write_text(header + "\n".join(LINES) + "\n", encoding="utf-8")
128 + print(f"\nReport written to {report_path}")
129 +
130 +
131 +if __name__ == "__main__":
132 + main()
added engine/scripts/03_clean.py +137 −0
@@ -0,0 +1,137 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/03_clean.py
7 +# Purpose : Step 3 — build the flagged clean table, write the full exclusion
8 +# table and cleaning report. Nothing is silently deleted.
9 +# =============================================================================
10 +"""Cleaning build (Execution Order step 3)."""
11 +
12 +from __future__ import annotations
13 +
14 +import sys
15 +from pathlib import Path
16 +
17 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
18 +
19 +import polars as pl
20 +
21 +from qwhpi.clean import (
22 + FLOORAREA_HI, FLOORAREA_LO, PRICE_CEILING, RATIO_HI, RATIO_LO,
23 + build_clean, research_sample,
24 +)
25 +from qwhpi.config import REPORTS_DIR, TABLES_DIR, ensure_dirs
26 +from qwhpi.geography import build_geo_join
27 +from qwhpi.ingest import load_raw
28 +
29 +LINES: list[str] = []
30 +
31 +
32 +def note(line: str = "") -> None:
33 + LINES.append(line)
34 + print(line)
35 +
36 +
37 +def main() -> None:
38 + ensure_dirs()
39 + tx = load_raw()
40 + geo = build_geo_join(tx)
41 + df = build_clean(tx, geo, force=True)
42 +
43 + note("## Cleaning report")
44 + note("")
45 + note(f"- Input rows: {df.height:,} (all retained; exclusions are flags)")
46 + note(f"- Screens: ratio band [{RATIO_LO}, {RATIO_HI}] on amount/totalArValue; "
47 + f"price ceiling ${PRICE_CEILING:,}; duplicate flags; "
48 + f"floorArea sanitation [{FLOORAREA_LO}, {FLOORAREA_HI}] m².")
49 + note("- Provider pre-filter verified: no transaction below $50,000 exists in the raw file.")
50 + note("")
51 +
52 + # ------------------------------------------------------------------ #
53 + # Exclusion table
54 + # ------------------------------------------------------------------ #
55 + excl = (
56 + df.filter(pl.col("exclude_reason").is_not_null())
57 + .group_by("exclude_reason").len()
58 + .rename({"len": "n"})
59 + .with_columns(pl.col("n").cast(pl.Int64))
60 + .with_columns((pl.col("n") / df.height * 100).round(3).alias("pct_of_raw"))
61 + .sort("n", descending=True)
62 + )
63 + total_excl = excl["n"].sum() if excl.height else 0
64 + excl_full = pl.concat([
65 + excl,
66 + pl.DataFrame({
67 + "exclude_reason": ["TOTAL_EXCLUDED", "RETAINED_IN_RESEARCH_SAMPLE"],
68 + "n": [total_excl, df.height - total_excl],
69 + "pct_of_raw": [round(total_excl / df.height * 100, 3),
70 + round((df.height - total_excl) / df.height * 100, 3)],
71 + }),
72 + ])
73 + excl_full.write_csv(TABLES_DIR / "exclusions.csv")
74 + note("### Exclusion table (outputs/tables/exclusions.csv)")
75 + for row in excl_full.iter_rows(named=True):
76 + note(f"- {row['exclude_reason']}: {row['n']:,} ({row['pct_of_raw']}%)")
77 +
78 + # Sanitation summary (rows kept, characteristic nulled).
79 + note("")
80 + note("### Characteristic sanitation (row retained, value nulled)")
81 + note(f"- floorArea outside [{FLOORAREA_LO}, {FLOORAREA_HI}] m²: "
82 + f"{df.filter(pl.col('fa_suspect')).height:,}")
83 + note(f"- yearBuilt > sale year + 1: {df.filter(pl.col('yb_suspect')).height:,}")
84 +
85 + # ------------------------------------------------------------------ #
86 + # Research sample summary
87 + # ------------------------------------------------------------------ #
88 + rs = research_sample(df)
89 + rs_all = research_sample(df, include_undetermined=True)
90 + note("")
91 + note("### Research sample")
92 + note(f"- Clean rows incl. indéterminé: {rs_all.height:,}")
93 + note(f"- Headline sample (3 determined types): {rs.height:,} "
94 + f"({rs.height / df.height * 100:.2f}% of raw)")
95 + by_type = rs.group_by("propertyType").len().sort("len", descending=True)
96 + for row in by_type.iter_rows(named=True):
97 + note(f" - {row['propertyType']}: {row['len']:,}")
98 +
99 + # Weekly volume of the research sample (continuity check).
100 + weekly = rs.group_by("week").len().sort("week")
101 + note(f"- Weeks with >=1 research-sample transaction: {weekly.height} / 291")
102 + note(f"- Median weekly research-sample volume: {weekly['len'].median():,.0f}")
103 +
104 + # Missingness within the research sample (for the hedonic step).
105 + miss = pl.DataFrame({
106 + "column": ["floorArea", "yearBuilt", "buildingType", "ownerType"],
107 + "pct_missing": [
108 + round(rs["floorArea"].is_null().mean() * 100, 2),
109 + round(rs["yearBuilt"].is_null().mean() * 100, 2),
110 + round(rs["buildingType"].is_null().mean() * 100, 2),
111 + round(rs["ownerType"].is_null().mean() * 100, 2),
112 + ],
113 + })
114 + miss.write_csv(TABLES_DIR / "research_sample_missingness.csv")
115 + note("")
116 + note("### Missingness in headline research sample")
117 + for row in miss.iter_rows(named=True):
118 + note(f"- {row['column']}: {row['pct_missing']}%")
119 +
120 + report_path = REPORTS_DIR / "cleaning_report.md"
121 + header = (
122 + "<!--\n"
123 + "=============================================================================\n"
124 + "QWHPI — Quebec Weekly Housing Price Index\n"
125 + "Author : Simon-Pierre Boucher\n"
126 + "Contact : contact@spboucher.ai\n"
127 + "File : outputs/reports/cleaning_report.md\n"
128 + "Purpose : Step 3 cleaning report (generated by engine/scripts/03_clean.py)\n"
129 + "=============================================================================\n"
130 + "-->\n\n"
131 + )
132 + report_path.write_text(header + "\n".join(LINES) + "\n", encoding="utf-8")
133 + print(f"\nReport written to {report_path}")
134 +
135 +
136 +if __name__ == "__main__":
137 + main()
added engine/scripts/04_baseline.py +98 −0
@@ -0,0 +1,98 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/04_baseline.py
7 +# Purpose : Step 4 — pooled hedonic time-dummy baseline for Quebec + the four
8 +# major cities × All/Unifamilial/Condo/Plex (20 cells).
9 +# =============================================================================
10 +"""Baseline index build (Execution Order step 4).
11 +
12 +For every cell: pooled Model A time-dummy regression (2021→present),
13 +week coefficients rebased to 2021 avg = 100, raw weekly median attached for
14 +comparison. Results land in ``data/interim/baseline_indexes.parquet``.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import sys
20 +import time
21 +from pathlib import Path
22 +
23 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
24 +
25 +import polars as pl
26 +
27 +from qwhpi.clean import CLEAN_PARQUET, research_sample
28 +from qwhpi.config import INTERIM_DIR, TABLES_DIR, ensure_dirs
29 +from qwhpi.features import build_features
30 +from qwhpi.hedonic import estimate_time_dummy
31 +
32 +BASELINE_PARQUET = INTERIM_DIR / "baseline_indexes.parquet"
33 +
34 +GEOGRAPHIES = [
35 + ("quebec", "Québec (province)", "province", None),
36 + ("montreal", "Montréal", "municipality", "Montréal"),
37 + ("quebec-city", "Québec", "municipality", "Québec"),
38 + ("laval", "Laval", "municipality", "Laval"),
39 + ("gatineau", "Gatineau", "municipality", "Gatineau"),
40 +]
41 +TYPE_SETS = ["all", "unifamilial", "condo", "plex"]
42 +
43 +
44 +def main() -> None:
45 + ensure_dirs()
46 + clean = pl.read_parquet(CLEAN_PARQUET)
47 + feat = build_features(research_sample(clean))
48 +
49 + frames: list[pl.DataFrame] = []
50 + summary_rows: list[dict] = []
51 +
52 + for geo_id, geo_name, level, mun_filter in GEOGRAPHIES:
53 + base = feat if mun_filter is None else feat.filter(pl.col("municipality") == mun_filter)
54 + for ptype in TYPE_SETS:
55 + cell = base if ptype == "all" else base.filter(pl.col("propertyType") == ptype)
56 + t0 = time.time()
57 + res = estimate_time_dummy(cell, absorb="loc_fine",
58 + include_property_type=(ptype == "all"))
59 + if res is None:
60 + print(f"[skip] {geo_id} x {ptype}: n={cell.height} too thin")
61 + summary_rows.append({"geography": geo_id, "property_type": ptype,
62 + "n_obs": cell.height, "estimated": False,
63 + "r2": None, "median_weekly_tx": None})
64 + continue
65 +
66 + # Raw weekly median + volume for comparison/validation.
67 + weekly_raw = (
68 + cell.group_by("week_str")
69 + .agg(pl.col("amount").median().alias("raw_median"),
70 + pl.len().alias("transactions"))
71 + .rename({"week_str": "week"})
72 + )
73 + tbl = (
74 + res.table.join(weekly_raw, on="week", how="left")
75 + .with_columns(
76 + pl.lit(geo_id).alias("geography_id"),
77 + pl.lit(geo_name).alias("geography_name"),
78 + pl.lit(level).alias("geography_level"),
79 + pl.lit(ptype).alias("property_type"),
80 + )
81 + )
82 + frames.append(tbl)
83 + med_tx = weekly_raw["transactions"].median()
84 + summary_rows.append({"geography": geo_id, "property_type": ptype,
85 + "n_obs": res.n_obs, "estimated": True,
86 + "r2": round(res.r2, 4),
87 + "median_weekly_tx": med_tx})
88 + print(f"[ok] {geo_id} x {ptype}: n={res.n_obs:,} r2={res.r2:.3f} "
89 + f"({time.time() - t0:.1f}s)")
90 +
91 + out = pl.concat(frames)
92 + out.write_parquet(BASELINE_PARQUET)
93 + pl.DataFrame(summary_rows).write_csv(TABLES_DIR / "baseline_model_summary.csv")
94 + print(f"\nSaved {out.height:,} index observations to {BASELINE_PARQUET}")
95 +
96 +
97 +if __name__ == "__main__":
98 + main()
added engine/scripts/05_hierarchical.py +230 −0
@@ -0,0 +1,230 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/05_hierarchical.py
7 +# Purpose : Step 5 — hierarchical weekly indexes: province, 17 regions and
8 +# target cities × type, with Kalman shrinkage for thin cells.
9 +# =============================================================================
10 +"""Hierarchical index build (Execution Order step 5).
11 +
12 +Persists stage-1 artifacts (paths, residuals, variances) for reuse by the
13 +validation and scaling steps, and writes
14 +``data/interim/hierarchical_indexes.parquet``.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import sys
20 +import time
21 +import unicodedata
22 +from pathlib import Path
23 +
24 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
25 +
26 +import numpy as np
27 +import polars as pl
28 +
29 +from qwhpi.clean import CLEAN_PARQUET, research_sample
30 +from qwhpi.config import CITY_TARGETS, INTERIM_DIR, PROPERTY_TYPES, TABLES_DIR, ensure_dirs
31 +from qwhpi.features import build_features
32 +from qwhpi.hierarchy import (
33 + CellDeviation, cell_deviation, compose_cell_table, rebase_log,
34 + stage1_residualize, week_grid,
35 +)
36 +
37 +HIER_PARQUET = INTERIM_DIR / "hierarchical_indexes.parquet"
38 +STAGE1_RESID = INTERIM_DIR / "stage1_residuals.parquet"
39 +STAGE1_PATHS = INTERIM_DIR / "stage1_paths.parquet"
40 +
41 +
42 +def slug(name: str) -> str:
43 + s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
44 + return s.lower().replace(" ", "-").replace("'", "")
45 +
46 +
47 +def city_slug(name: str) -> str:
48 + # Municipality 'Québec' must not collide with the province id.
49 + return "quebec-city" if name == "Québec" else slug(name)
50 +
51 +
52 +def aligned_path(paths: pl.DataFrame, ptype: str, grid: list[str]) -> np.ndarray:
53 + sub = {r["week"]: r["delta"] for r in
54 + paths.filter(pl.col("property_type") == ptype).iter_rows(named=True)}
55 + arr = np.array([sub.get(w, np.nan) for w in grid])
56 + # Interpolate the rare missing week levels (singleton-dropped in stage 1).
57 + if np.isnan(arr).any():
58 + idx = np.arange(len(arr))
59 + ok = np.isfinite(arr)
60 + arr = np.interp(idx, idx[ok], arr[ok])
61 + return arr
62 +
63 +
64 +def combine_all(tables: dict[str, pl.DataFrame], weights: dict[str, float],
65 + geography_id: str, geography_name: str,
66 + geography_level: str) -> pl.DataFrame:
67 + """Fixed-share composition of the three type indexes into 'all'."""
68 + wsum = sum(weights.values())
69 + w = {k: v / wsum for k, v in weights.items()}
70 + base = tables[next(iter(tables))].sort("week")
71 + grid = base["week"].to_list()
72 +
73 + def wavg(col: str, log: bool = True) -> np.ndarray:
74 + acc = np.zeros(len(grid))
75 + for k, tbl in tables.items():
76 + vals = tbl.sort("week")[col].to_numpy()
77 + acc += w[k] * (np.log(vals / 100.0) if log else vals)
78 + return acc
79 +
80 + log_raw = wavg("index")
81 + log_filt = wavg("index_smoothed")
82 + log_res = wavg("index_research")
83 + var = np.zeros(len(grid))
84 + for k, tbl in tables.items():
85 + var += (w[k] ** 2) * (tbl.sort("week")["se_log"].to_numpy() ** 2)
86 + se = np.sqrt(var)
87 + n = sum(np.nan_to_num(tbl.sort("week")["transactions"].to_numpy())
88 + for tbl in tables.values())
89 + shrink = wavg("shrinkage_weight", log=False)
90 +
91 + return pl.DataFrame({
92 + "week": grid,
93 + "geography_id": geography_id,
94 + "geography_name": geography_name,
95 + "geography_level": geography_level,
96 + "property_type": "all",
97 + "index": np.exp(log_raw) * 100.0,
98 + "index_smoothed": np.exp(log_filt) * 100.0,
99 + "index_research": np.exp(log_res) * 100.0,
100 + "log_index": log_filt,
101 + "se_log": se,
102 + "lower_95": np.exp(log_filt - 1.96 * se) * 100.0,
103 + "upper_95": np.exp(log_filt + 1.96 * se) * 100.0,
104 + "transactions": n,
105 + "shrinkage_weight": shrink,
106 + "research_var": var,
107 + })
108 +
109 +
110 +def main() -> None:
111 + ensure_dirs()
112 + feat = build_features(research_sample(pl.read_parquet(CLEAN_PARQUET)))
113 +
114 + print("Stage 1: pooled residualization ...")
115 + t0 = time.time()
116 + s1 = stage1_residualize(feat)
117 + print(f" n={s1.n_obs:,} r2={s1.r2:.4f} ({time.time() - t0:.0f}s)")
118 + s1.residuals.write_parquet(STAGE1_RESID)
119 + s1.paths.write_parquet(STAGE1_PATHS)
120 +
121 + resid = s1.residuals
122 + grid = week_grid(resid)
123 + T = len(grid)
124 +
125 + # Type-level residual variances (fallback for tiny cells) and weekly
126 + # counts for the province path variance.
127 + sigma2_type = {t: float(resid.filter(pl.col("propertyType") == t)["resid"].var())
128 + for t in PROPERTY_TYPES}
129 +
130 + frames: list[pl.DataFrame] = []
131 + region_dev_store: dict[tuple[str, str], CellDeviation] = {}
132 +
133 + region_names = dict(
134 + feat.select("region_code", "region").unique().iter_rows()
135 + )
136 +
137 + for ptype in PROPERTY_TYPES:
138 + path = aligned_path(s1.paths, ptype, grid)
139 + r_t = resid.filter(pl.col("propertyType") == ptype)
140 + counts = {r["week_str"]: r["n"] for r in
141 + r_t.group_by("week_str").agg(pl.len().alias("n")).iter_rows(named=True)}
142 + n_prov = np.array([counts.get(w, 0) for w in grid], dtype=float)
143 + path_var = np.where(n_prov > 0, sigma2_type[ptype] / np.maximum(n_prov, 1), np.nan)
144 + path_var = np.nan_to_num(path_var, nan=np.nanmax(path_var))
145 +
146 + # ---- province cell ---- #
147 + prov = compose_cell_table(
148 + grid=grid, province_path=path, deviations=[],
149 + geography_id="quebec", geography_name="Québec (province)",
150 + geography_level="province", property_type=ptype, path_var=path_var,
151 + ).with_columns(
152 + pl.Series("transactions", n_prov),
153 + pl.Series("shrinkage_weight", np.zeros(T)),
154 + )
155 + frames.append(prov)
156 +
157 + # ---- 17 regions ---- #
158 + for rcode in sorted(region_names):
159 + cell = r_t.filter(pl.col("region_code") == rcode).rename({"resid": "dev"})
160 + dev = cell_deviation(cell.select("week_str", "dev"), grid, sigma2_type[ptype])
161 + region_dev_store[(rcode, ptype)] = dev
162 + frames.append(compose_cell_table(
163 + grid=grid, province_path=path, deviations=[dev],
164 + geography_id=f"region-{rcode}",
165 + geography_name=region_names[rcode],
166 + geography_level="region", property_type=ptype, path_var=path_var,
167 + ))
168 +
169 + # ---- target cities ---- #
170 + for city in CITY_TARGETS:
171 + cell = r_t.filter(pl.col("municipality") == city)
172 + if cell.height == 0:
173 + continue
174 + rcode = cell["region_code"][0]
175 + rdev = region_dev_store[(rcode, ptype)]
176 + rpath = {w: v for w, v in zip(grid, rdev.fit.smoothed)}
177 + cell = cell.with_columns(
178 + (pl.col("resid") - pl.col("week_str").replace_strict(rpath, default=0.0))
179 + .alias("dev")
180 + )
181 + mdev = cell_deviation(cell.select("week_str", "dev"), grid, sigma2_type[ptype])
182 + frames.append(compose_cell_table(
183 + grid=grid, province_path=path, deviations=[rdev, mdev],
184 + geography_id=city_slug(city), geography_name=city,
185 + geography_level="municipality", property_type=ptype, path_var=path_var,
186 + ))
187 +
188 + out = pl.concat(frames)
189 +
190 + # ---- 'all' composition per geography (fixed full-sample type shares) ---- #
191 + all_frames: list[pl.DataFrame] = []
192 + geos = out.select("geography_id", "geography_name", "geography_level").unique()
193 + for gid, gname, glevel in geos.iter_rows():
194 + tables = {
195 + t: out.filter((pl.col("geography_id") == gid) & (pl.col("property_type") == t))
196 + for t in PROPERTY_TYPES
197 + }
198 + tables = {t: tbl for t, tbl in tables.items() if tbl.height}
199 + weights = {t: float(np.nansum(tbl["transactions"].to_numpy()))
200 + for t, tbl in tables.items()}
201 + weights = {t: max(v, 1.0) for t, v in weights.items()}
202 + all_frames.append(combine_all(tables, weights, gid, gname, glevel))
203 +
204 + out = pl.concat([out, *all_frames]).sort(
205 + ["geography_level", "geography_id", "property_type", "week"])
206 + out.write_parquet(HIER_PARQUET)
207 +
208 + # Summary: volatility comparison raw vs smoothed for thin cells.
209 + rows = []
210 + for (gid, ptype), grp in out.to_pandas().groupby(["geography_id", "property_type"]):
211 + grp = grp.sort_values("week")
212 + li_raw = np.log(grp["index"].to_numpy())
213 + li_smo = np.log(grp["index_smoothed"].to_numpy())
214 + ok = np.isfinite(li_raw)
215 + rows.append({
216 + "geography": gid, "property_type": ptype,
217 + "median_weekly_tx": float(np.nanmedian(grp["transactions"])),
218 + "sd_raw_pct": round(float(np.nanstd(np.diff(li_raw[ok]))) * 100, 2),
219 + "sd_smoothed_pct": round(float(np.std(np.diff(li_smo))) * 100, 2),
220 + "mean_shrinkage": round(float(np.nanmean(grp["shrinkage_weight"])), 3),
221 + "last_index": round(float(grp["index_smoothed"].iloc[-1]), 2),
222 + })
223 + summary = pl.DataFrame(rows).sort(["geography", "property_type"])
224 + summary.write_csv(TABLES_DIR / "hierarchical_summary.csv")
225 + print(summary)
226 + print(f"\nSaved {out.height:,} observations to {HIER_PARQUET}")
227 +
228 +
229 +if __name__ == "__main__":
230 + main()
added engine/scripts/06_validation.py +271 −0
@@ -0,0 +1,271 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/06_validation.py
7 +# Purpose : Step 6 — repeat-sales comparison, downsampling experiment,
8 +# composition-shock simulation, weekly-vs-monthly comparison.
9 +# =============================================================================
10 +"""Validation suite (Execution Order step 6). Writes tables + report."""
11 +
12 +from __future__ import annotations
13 +
14 +import sys
15 +from pathlib import Path
16 +
17 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
18 +
19 +import numpy as np
20 +import polars as pl
21 +
22 +from qwhpi.clean import CLEAN_PARQUET, research_sample
23 +from qwhpi.config import REPORTS_DIR, TABLES_DIR, ensure_dirs
24 +from qwhpi.features import build_features
25 +from qwhpi.hierarchy import cell_deviation, week_grid
26 +from qwhpi.repeat_sales import bmn_index, build_pairs
27 +
28 +RNG = np.random.default_rng(42)
29 +LINES: list[str] = []
30 +
31 +
32 +def note(line: str = "") -> None:
33 + LINES.append(line)
34 + print(line)
35 +
36 +
37 +# ------------------------------------------------------------------------- #
38 +# 1. Repeat-sales comparison
39 +# ------------------------------------------------------------------------- #
40 +
41 +def repeat_sales_comparison(cl: pl.DataFrame, hier: pl.DataFrame) -> None:
42 + note("## 1. Repeat-sales (BMN) vs hedonic index")
43 + note("")
44 + cells = [
45 + ("quebec", None, "unifamilial"), ("quebec", None, "condo"),
46 + ("quebec", None, "plex"),
47 + ("montreal", "Montréal", "condo"), ("montreal", "Montréal", "unifamilial"),
48 + ("quebec-city", "Québec", "unifamilial"),
49 + ]
50 + rows = []
51 + for gid, mun, ptype in cells:
52 + sub = cl.filter(pl.col("propertyType") == ptype)
53 + if mun:
54 + sub = sub.filter(pl.col("municipality") == mun)
55 + rs = bmn_index(build_pairs(sub))
56 + if rs is None:
57 + continue
58 + h = (
59 + hier.filter((pl.col("geography_id") == gid)
60 + & (pl.col("property_type") == ptype))
61 + .with_columns(pl.col("week").str.slice(0, 7).alias("month"))
62 + .group_by("month").agg(pl.col("index_research").mean().alias("hedonic"))
63 + )
64 + j = rs.join(h, on="month", how="inner").sort("month")
65 + lg_rs = np.log(j["rs_index"].to_numpy())
66 + lg_he = np.log(j["hedonic"].to_numpy())
67 + corr = float(np.corrcoef(np.diff(lg_rs), np.diff(lg_he))[0, 1])
68 + rows.append({
69 + "cell": f"{gid}/{ptype}", "n_pairs": build_pairs(sub).height,
70 + "corr_monthly_changes": round(corr, 3),
71 + "end_gap_pct": round((lg_he[-1] - lg_rs[-1]) * 100, 2),
72 + "rs_end": round(float(j["rs_index"][-1]), 1),
73 + "hedonic_end": round(float(j["hedonic"][-1]), 1),
74 + })
75 + note(f"- {gid}/{ptype}: Δcorr={corr:.3f}, end levels RS={j['rs_index'][-1]:.1f} "
76 + f"vs hedonic={j['hedonic'][-1]:.1f}")
77 + pl.DataFrame(rows).write_csv(TABLES_DIR / "repeat_sales_comparison.csv")
78 +
79 +
80 +# ------------------------------------------------------------------------- #
81 +# 2. Downsampling experiment (Montreal condo lab)
82 +# ------------------------------------------------------------------------- #
83 +
84 +def downsampling(resid: pl.DataFrame, n_reps: int = 20) -> None:
85 + note("")
86 + note("## 2. Downsampling experiment — Montréal condo lab")
87 + note("")
88 + cell = resid.filter((pl.col("municipality") == "Montréal")
89 + & (pl.col("propertyType") == "condo"))
90 + grid = week_grid(resid)
91 + sigma2 = float(cell["resid"].var())
92 +
93 + ref = cell_deviation(cell.rename({"resid": "dev"}).select("week_str", "dev"),
94 + grid, sigma2)
95 + ref_path = ref.fit.smoothed
96 + ref_turn = np.sign(np.diff(_ma(ref_path, 4)))
97 +
98 + rows = []
99 + for target in [100, 50, 25, 15, 10, 5]:
100 + rmse_l, bias_l, vol_l, turn_l, cov_l = [], [], [], [], []
101 + for _ in range(n_reps):
102 + thin = (
103 + cell.with_columns(pl.lit(RNG.random(cell.height)).alias("_u"))
104 + .with_columns(pl.col("_u").rank().over("week_str").alias("_rk"))
105 + .filter(pl.col("_rk") <= target)
106 + )
107 + fit = cell_deviation(thin.rename({"resid": "dev"}).select("week_str", "dev"),
108 + grid, sigma2)
109 + path = fit.fit.smoothed
110 + err = path - ref_path
111 + rmse_l.append(np.sqrt(np.mean(err ** 2)))
112 + bias_l.append(np.mean(err))
113 + vol_l.append(np.std(np.diff(path)) / max(np.std(np.diff(ref_path)), 1e-12))
114 + turn = np.sign(np.diff(_ma(path, 4)))
115 + turn_l.append(float(np.mean(turn == ref_turn)))
116 + band = 1.96 * np.sqrt(fit.fit.smoothed_var)
117 + cov_l.append(float(np.mean(np.abs(err) <= band)))
118 + rows.append({
119 + "target_tx_per_week": target,
120 + "rmse_log_pct": round(float(np.mean(rmse_l)) * 100, 3),
121 + "bias_log_pct": round(float(np.mean(bias_l)) * 100, 3),
122 + "volatility_ratio": round(float(np.mean(vol_l)), 3),
123 + "turning_point_agreement": round(float(np.mean(turn_l)), 3),
124 + "ci95_coverage": round(float(np.mean(cov_l)), 3),
125 + })
126 + note(f"- {target:>3} tx/wk: RMSE={rows[-1]['rmse_log_pct']}% "
127 + f"bias={rows[-1]['bias_log_pct']}% vol_ratio={rows[-1]['volatility_ratio']} "
128 + f"turn_agree={rows[-1]['turning_point_agreement']} "
129 + f"CI95_cov={rows[-1]['ci95_coverage']}")
130 + pl.DataFrame(rows).write_csv(TABLES_DIR / "downsampling_results.csv")
131 + note("")
132 + note("Reading: RMSE vs the full-sample (~145 tx/wk) smoothed path; CI "
133 + "coverage of the deviation posterior; turning points on 4-week MA.")
134 +
135 +
136 +def _ma(x: np.ndarray, w: int) -> np.ndarray:
137 + return np.convolve(x, np.ones(w) / w, mode="same")
138 +
139 +
140 +# ------------------------------------------------------------------------- #
141 +# 3. Composition-shock simulation
142 +# ------------------------------------------------------------------------- #
143 +
144 +def composition_shock(feat: pl.DataFrame) -> None:
145 + note("")
146 + note("## 3. Composition-shock simulation (province unifamilial)")
147 + note("")
148 + import pyfixest as pf
149 +
150 + uni = feat.filter(pl.col("propertyType") == "unifamilial")
151 + med_fa = float(uni["floor_area_filled"].median())
152 + shock_weeks = [w for w in uni["week_str"].unique().to_list()
153 + if w.startswith("2024") and w < "2024-07"]
154 +
155 + # Drop 70% of below-median-floorArea sales in shocked weeks.
156 + u = RNG.random(uni.height)
157 + uni = uni.with_columns(pl.lit(u).alias("_u"))
158 + shocked = uni.filter(
159 + ~(
160 + pl.col("week_str").is_in(shock_weeks)
161 + & (pl.col("floor_area_filled") < med_fa)
162 + & (pl.col("_u") < 0.7)
163 + )
164 + )
165 + note(f"- Shock: drop 70% of below-median floorArea sales in 2024-H1 "
166 + f"({uni.height - shocked.height:,} of {uni.height:,} rows removed)")
167 +
168 + def weekly_median(df: pl.DataFrame) -> dict[str, float]:
169 + return {r["week_str"]: r["m"] for r in
170 + df.group_by("week_str").agg(pl.col("amount").median().alias("m"))
171 + .iter_rows(named=True)}
172 +
173 + def hedonic_path(df: pl.DataFrame) -> dict[str, float]:
174 + pdf = df.select("log_amount", "week_str", "log_fa", "fa_missing",
175 + "age_bin", "building_type", "loc_fine").to_pandas()
176 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
177 + fit = pf.feols("log_amount ~ log_fa + fa_missing + C(age_bin) "
178 + "| loc_fine + week_str", data=pdf)
179 + fx = fit.fixef()
180 + key = next(k for k in fx if "week_str" in k)
181 + return {k: float(v) for k, v in fx[key].items()}
182 +
183 + med_b, med_s = weekly_median(uni), weekly_median(shocked)
184 + hed_b, hed_s = hedonic_path(uni), hedonic_path(shocked)
185 +
186 + common = [w for w in shock_weeks if w in med_b and w in med_s
187 + and w in hed_b and w in hed_s]
188 + dmed = np.mean([np.log(med_s[w] / med_b[w]) for w in common]) * 100
189 + dhed = np.mean([hed_s[w] - hed_b[w] for w in common]) * 100
190 + note(f"- Raw weekly median moves by {dmed:+.2f}% in shocked weeks (must move)")
191 + note(f"- Hedonic time-dummy moves by {dhed:+.2f}% (must stay ~0)")
192 + pl.DataFrame({
193 + "metric": ["raw_median_shift_pct", "hedonic_shift_pct"],
194 + "value": [round(float(dmed), 3), round(float(dhed), 3)],
195 + }).write_csv(TABLES_DIR / "composition_shock.csv")
196 +
197 +
198 +# ------------------------------------------------------------------------- #
199 +# 4. Weekly vs monthly comparison
200 +# ------------------------------------------------------------------------- #
201 +
202 +def weekly_vs_monthly(hier: pl.DataFrame) -> None:
203 + note("")
204 + note("## 4. Weekly vs monthly (same methodology)")
205 + note("")
206 + rows = []
207 + for gid, ptype in [("quebec", "unifamilial"), ("quebec", "condo"),
208 + ("montreal", "condo")]:
209 + h = hier.filter((pl.col("geography_id") == gid)
210 + & (pl.col("property_type") == ptype)).sort("week")
211 + wk_raw = np.log(h["index"].to_numpy())
212 + wk_smooth = np.log(h["index_smoothed"].to_numpy())
213 + wk_res = np.log(h["index_research"].to_numpy())
214 + monthly = (
215 + h.with_columns(pl.col("week").str.slice(0, 7).alias("m"))
216 + .group_by("m").agg(pl.col("index_research").mean().alias("ix"))
217 + .sort("m")
218 + )
219 + mo = np.log(monthly["ix"].to_numpy())
220 + # noise = raw minus final smoothed; signal = final smoothed changes
221 + noise_sd = float(np.nanstd(wk_raw - wk_res))
222 + signal_sd = float(np.std(np.diff(wk_res)))
223 + revision_sd = float(np.std(wk_smooth - wk_res)) # real-time vs final
224 + rows.append({
225 + "cell": f"{gid}/{ptype}",
226 + "weekly_signal_sd_pct": round(signal_sd * 100, 3),
227 + "weekly_noise_sd_pct": round(noise_sd * 100, 3),
228 + "monthly_change_sd_pct": round(float(np.std(np.diff(mo))) * 100, 3),
229 + "realtime_revision_sd_pct": round(revision_sd * 100, 3),
230 + "weeks_to_monthly_ratio": round(signal_sd * np.sqrt(52 / 12)
231 + / max(float(np.std(np.diff(mo))), 1e-9), 2),
232 + })
233 + note(f"- {gid}/{ptype}: weekly signal SD={rows[-1]['weekly_signal_sd_pct']}%, "
234 + f"noise SD={rows[-1]['weekly_noise_sd_pct']}%, monthly ΔSD="
235 + f"{rows[-1]['monthly_change_sd_pct']}%, real-time revision SD="
236 + f"{rows[-1]['realtime_revision_sd_pct']}%")
237 + pl.DataFrame(rows).write_csv(TABLES_DIR / "weekly_vs_monthly.csv")
238 +
239 +
240 +def main() -> None:
241 + ensure_dirs()
242 + cl = research_sample(pl.read_parquet(CLEAN_PARQUET))
243 + feat = build_features(cl)
244 + hier = pl.read_parquet("data/interim/hierarchical_indexes.parquet")
245 + resid = pl.read_parquet("data/interim/stage1_residuals.parquet")
246 +
247 + note("# QWHPI validation report")
248 + note("")
249 + repeat_sales_comparison(cl, hier)
250 + downsampling(resid)
251 + composition_shock(feat)
252 + weekly_vs_monthly(hier)
253 +
254 + header = (
255 + "<!--\n"
256 + "=============================================================================\n"
257 + "QWHPI — Quebec Weekly Housing Price Index\n"
258 + "Author : Simon-Pierre Boucher\n"
259 + "Contact : contact@spboucher.ai\n"
260 + "File : outputs/reports/validation_report.md\n"
261 + "Purpose : Step 6 validation report (generated by 06_validation.py)\n"
262 + "=============================================================================\n"
263 + "-->\n\n"
264 + )
265 + (REPORTS_DIR / "validation_report.md").write_text(
266 + header + "\n".join(LINES) + "\n", encoding="utf-8")
267 + print("\nReport written to outputs/reports/validation_report.md")
268 +
269 +
270 +if __name__ == "__main__":
271 + main()
added engine/scripts/07_canonical.py +228 −0
@@ -0,0 +1,228 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/07_canonical.py
7 +# Purpose : Step 7 — assemble data/processed/qwhpi_weekly.parquet (canonical
8 +# dataset), coverage matrix, assessment gap, vintages, liquidity.
9 +# =============================================================================
10 +"""Canonical dataset build (Execution Order step 7, schema §4.9)."""
11 +
12 +from __future__ import annotations
13 +
14 +import datetime as dt
15 +import sys
16 +import unicodedata
17 +from pathlib import Path
18 +
19 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
20 +
21 +import numpy as np
22 +import polars as pl
23 +
24 +from qwhpi.clean import CLEAN_PARQUET, research_sample
25 +from qwhpi.config import (
26 + CITY_TARGETS, INTERIM_DIR, PROCESSED_DIR, PROPERTY_TYPES, TABLES_DIR,
27 + ensure_dirs,
28 +)
29 +from qwhpi.index import (
30 + add_growth, add_reliability, add_representative_value, compute_basket,
31 +)
32 +from qwhpi.nowcast import partial_week_flags
33 +from qwhpi.seasonal import seasonality_test
34 +from qwhpi.vintages import snapshot, update_first_release
35 +
36 +MODEL_VERSION = "0.1.0"
37 +CANONICAL = PROCESSED_DIR / "qwhpi_weekly.parquet"
38 +
39 +
40 +def slug(name: str) -> str:
41 + s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
42 + return s.lower().replace(" ", "-").replace("'", "")
43 +
44 +
45 +def city_slug(name: str) -> str:
46 + return "quebec-city" if name == "Québec" else slug(name)
47 +
48 +
49 +def main() -> None:
50 + ensure_dirs()
51 + data_vintage = dt.date.today().isoformat()
52 + hier = pl.read_parquet(INTERIM_DIR / "hierarchical_indexes.parquet")
53 + cl = research_sample(pl.read_parquet(CLEAN_PARQUET))
54 + cl = cl.with_columns(pl.col("week").dt.strftime("%Y-%m-%d").alias("week_str"))
55 +
56 + # ------------------------------------------------------------------ #
57 + # Map transactions to geography_ids (for baskets & liquidity)
58 + # ------------------------------------------------------------------ #
59 + cell_frames = []
60 + for gid_expr, filt in [
61 + (pl.lit("quebec"), pl.lit(True)),
62 + (pl.concat_str([pl.lit("region-"), pl.col("region_code")]),
63 + pl.col("region_code").is_not_null()),
64 + ]:
65 + cell_frames.append(
66 + cl.filter(filt).select(
67 + gid_expr.alias("geography_id"),
68 + pl.col("propertyType").alias("property_type"),
69 + pl.col("week_str").alias("week"),
70 + "log_amount", "amount",
71 + )
72 + )
73 + for city in CITY_TARGETS:
74 + sub = cl.filter(pl.col("municipality") == city)
75 + if sub.height:
76 + cell_frames.append(sub.select(
77 + pl.lit(city_slug(city)).alias("geography_id"),
78 + pl.col("propertyType").alias("property_type"),
79 + pl.col("week_str").alias("week"),
80 + "log_amount", "amount",
81 + ))
82 + tx_cells = pl.concat(cell_frames)
83 + tx_cells_all = pl.concat([
84 + tx_cells,
85 + tx_cells.with_columns(pl.lit("all").alias("property_type")),
86 + ])
87 +
88 + # ------------------------------------------------------------------ #
89 + # Canonical columns
90 + # ------------------------------------------------------------------ #
91 + basket = compute_basket(tx_cells_all, hier)
92 + canon = add_representative_value(hier, basket)
93 + canon = add_growth(canon)
94 + canon = add_reliability(canon)
95 +
96 + prov_vol = (
97 + cl.group_by("week_str").len()
98 + .rename({"week_str": "week", "len": "volume"})
99 + )
100 + canon = canon.join(partial_week_flags(prov_vol), on="week", how="left") \
101 + .with_columns(pl.col("is_partial_week").fill_null(False))
102 +
103 + # Effective sample size: information units of the filtered estimate.
104 + sigma2_map = {
105 + t: float(pl.read_parquet(INTERIM_DIR / "stage1_residuals.parquet")
106 + .filter(pl.col("propertyType") == t)["resid"].var())
107 + for t in PROPERTY_TYPES
108 + }
109 + sigma2_map["all"] = float(np.mean(list(sigma2_map.values())))
110 + canon = canon.with_columns(
111 + pl.col("property_type").replace_strict(sigma2_map, default=None)
112 + .alias("_sigma2")
113 + ).with_columns(
114 + (pl.col("_sigma2") / (pl.col("se_log") ** 2).clip(1e-9, None))
115 + .round(1).alias("effective_sample_size")
116 + ).drop("_sigma2")
117 +
118 + canon = canon.with_columns(
119 + pl.lit(MODEL_VERSION).alias("model_version"),
120 + pl.lit(data_vintage).alias("data_vintage"),
121 + pl.col("transactions").fill_nan(0).cast(pl.Int64),
122 + )
123 +
124 + cols = ["week", "geography_level", "geography_id", "geography_name",
125 + "property_type", "index", "index_smoothed", "representative_value",
126 + "transactions", "effective_sample_size",
127 + "weekly_pct", "four_week_pct", "thirteen_week_pct",
128 + "twenty_six_week_pct", "yoy_pct",
129 + "lower_95", "upper_95", "reliability_grade", "shrinkage_weight",
130 + "is_partial_week", "model_version", "data_vintage"]
131 + canonical = canon.select(cols).sort(
132 + ["geography_level", "geography_id", "property_type", "week"])
133 + canonical.write_parquet(CANONICAL)
134 + print(f"Canonical table: {canonical.height:,} rows -> {CANONICAL}")
135 +
136 + # ------------------------------------------------------------------ #
137 + # Coverage matrix (all municipalities × types)
138 + # ------------------------------------------------------------------ #
139 + weeks_total = cl["week_str"].n_unique()
140 + liq = (
141 + cl.group_by(["municipality", "geo_code", "region", "propertyType", "week_str"])
142 + .len()
143 + .group_by(["municipality", "geo_code", "region", "propertyType"])
144 + .agg(pl.col("len").median().alias("median_weekly_tx"),
145 + pl.col("len").sum().alias("total_tx"),
146 + (pl.len() / weeks_total * 100).round(1).alias("pct_weeks_active"))
147 + )
148 + published_cities = {city_slug(c) for c in CITY_TARGETS}
149 + liq = liq.with_columns(
150 + pl.when(pl.col("municipality").is_in(list(CITY_TARGETS)))
151 + .then(pl.lit("published"))
152 + .when((pl.col("median_weekly_tx") >= 5) & (pl.col("pct_weeks_active") >= 80))
153 + .then(pl.lit("conditional"))
154 + .otherwise(pl.lit("not_published"))
155 + .alias("coverage_status")
156 + ).sort("total_tx", descending=True)
157 + liq.write_csv(TABLES_DIR / "coverage.csv")
158 + liq.write_parquet(PROCESSED_DIR / "coverage_matrix.parquet")
159 + counts = liq.group_by("coverage_status").len()
160 + print("Coverage matrix:", dict(counts.iter_rows()))
161 +
162 + # Weekly liquidity per published cell.
163 + weekly_liq = (
164 + tx_cells_all.group_by(["geography_id", "property_type", "week"]).len()
165 + .rename({"len": "transactions"}).sort(["geography_id", "property_type", "week"])
166 + )
167 + weekly_liq.write_parquet(PROCESSED_DIR / "weekly_liquidity.parquet")
168 + (
169 + weekly_liq.group_by(["geography_id", "property_type"])
170 + .agg(pl.col("transactions").median().alias("median"),
171 + pl.col("transactions").mean().round(1).alias("mean"))
172 + .sort(["geography_id", "property_type"])
173 + .write_csv(TABLES_DIR / "weekly_liquidity.csv")
174 + )
175 +
176 + # ------------------------------------------------------------------ #
177 + # Assessment Gap Index (secondary module — never mixed with QWHPI)
178 + # ------------------------------------------------------------------ #
179 + gap = (
180 + cl.filter(pl.col("totalArValue") > 0)
181 + .with_columns((pl.col("amount") / pl.col("totalArValue")).alias("ratio"))
182 + )
183 + gap_cells = pl.concat([
184 + gap.select(pl.lit("quebec").alias("geography_id"),
185 + pl.col("propertyType").alias("property_type"),
186 + pl.col("week_str").alias("week"), "ratio"),
187 + gap.select(pl.concat_str([pl.lit("region-"), pl.col("region_code")])
188 + .alias("geography_id"),
189 + pl.col("propertyType").alias("property_type"),
190 + pl.col("week_str").alias("week"), "ratio"),
191 + ])
192 + assessment = (
193 + gap_cells.group_by(["geography_id", "property_type", "week"])
194 + .agg(pl.col("ratio").median().round(4).alias("median_ratio"),
195 + pl.len().alias("n"))
196 + .sort(["geography_id", "property_type", "week"])
197 + )
198 + assessment.write_parquet(PROCESSED_DIR / "assessment_gap.parquet")
199 + print(f"Assessment gap: {assessment.height:,} rows")
200 +
201 + # ------------------------------------------------------------------ #
202 + # Seasonality check (documents the NSA decision)
203 + # ------------------------------------------------------------------ #
204 + seas_rows = []
205 + for gid, ptype in [("quebec", "all"), ("quebec", "unifamilial"),
206 + ("montreal", "condo")]:
207 + s = canonical.filter((pl.col("geography_id") == gid)
208 + & (pl.col("property_type") == ptype)).sort("week")
209 + res = seasonality_test(np.log(s["index_smoothed"].to_numpy()))
210 + seas_rows.append({"cell": f"{gid}/{ptype}", **res})
211 + seas = pl.DataFrame(seas_rows)
212 + seas.write_csv(TABLES_DIR / "seasonality_tests.csv")
213 + print(seas)
214 +
215 + # ------------------------------------------------------------------ #
216 + # Vintages + latest snapshot table
217 + # ------------------------------------------------------------------ #
218 + snapshot(canonical, data_vintage)
219 + update_first_release(canonical, data_vintage)
220 +
221 + latest_week = canonical.filter(~pl.col("is_partial_week"))["week"].max()
222 + latest = canonical.filter(pl.col("week") == latest_week)
223 + latest.write_csv(TABLES_DIR / "index_latest.csv")
224 + print(f"Latest complete week: {latest_week} | {latest.height} series")
225 +
226 +
227 +if __name__ == "__main__":
228 + main()
added engine/scripts/08_figures.py +183 −0
@@ -0,0 +1,183 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/08_figures.py
7 +# Purpose : Publication figures — aggregate, regions, cities, median-vs-
8 +# hedonic, repeat sales, downsampling, volumes, YoY map, gap.
9 +# =============================================================================
10 +"""Research figures (Execution Order step 10)."""
11 +
12 +from __future__ import annotations
13 +
14 +import sys
15 +from pathlib import Path
16 +
17 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
18 +
19 +import numpy as np
20 +import polars as pl
21 +
22 +from qwhpi import plotting as P
23 +from qwhpi.config import FIGURES_DIR, PROCESSED_DIR, TABLES_DIR, ensure_dirs
24 +
25 +import matplotlib.dates as mdates # noqa: E402
26 +import matplotlib.pyplot as plt # noqa: E402
27 +
28 +
29 +def dts(weeks: pl.Series) -> np.ndarray:
30 + return weeks.str.to_date("%Y-%m-%d").to_numpy()
31 +
32 +
33 +def cell(c: pl.DataFrame, gid: str, t: str) -> pl.DataFrame:
34 + return c.filter((pl.col("geography_id") == gid)
35 + & (pl.col("property_type") == t)).sort("week")
36 +
37 +
38 +def main() -> None:
39 + ensure_dirs()
40 + c = pl.read_parquet(PROCESSED_DIR / "qwhpi_weekly.parquet")
41 +
42 + # 1 — Quebec aggregate with CI band
43 + q = cell(c, "quebec", "all")
44 + fig, ax = plt.subplots(figsize=(9, 4.5))
45 + x = dts(q["week"])
46 + ax.fill_between(x, q["lower_95"], q["upper_95"], color=P.BAND, label="95% CI")
47 + ax.plot(x, q["index_smoothed"], color=P.SERIES[0], label="QWHPI-QC (smoothed)")
48 + ax.plot(x, q["index"], color=P.SERIES[0], alpha=0.35, lw=1, label="raw weekly")
49 + ax.set_title("Quebec weekly housing price index — all types (2021 = 100)")
50 + ax.legend(loc="upper left")
51 + ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
52 + P.save(fig, FIGURES_DIR / "quebec_aggregate.png")
53 +
54 + # 2 — 17 regions, small multiples
55 + regs = sorted(c.filter(pl.col("geography_level") == "region")["geography_id"].unique())
56 + fig, axes = plt.subplots(3, 6, figsize=(15, 7), sharex=True, sharey=True)
57 + for ax, rid in zip(axes.flat, regs):
58 + r = cell(c, rid, "all")
59 + ax.plot(dts(r["week"]), r["index_smoothed"], color=P.SERIES[0], lw=1.4)
60 + ax.set_title(r["geography_name"][0], fontsize=8)
61 + ax.xaxis.set_major_formatter(mdates.DateFormatter("%y"))
62 + for ax in axes.flat[len(regs):]:
63 + ax.axis("off")
64 + fig.suptitle("Regional indexes — all types (2021 = 100)")
65 + P.save(fig, FIGURES_DIR / "region_comparison.png")
66 +
67 + # 3 — Big-4 cities
68 + fig, ax = plt.subplots(figsize=(9, 4.5))
69 + for i, (gid, label) in enumerate([("montreal", "Montréal"), ("quebec-city", "Québec"),
70 + ("laval", "Laval"), ("gatineau", "Gatineau")]):
71 + s = cell(c, gid, "all")
72 + ax.plot(dts(s["week"]), s["index_smoothed"], color=P.SERIES[i], label=label)
73 + ax.set_title("Major cities — all types (2021 = 100)")
74 + ax.legend(loc="upper left")
75 + P.save(fig, FIGURES_DIR / "big4_cities.png")
76 +
77 + # 4 — Condo across cities
78 + fig, ax = plt.subplots(figsize=(9, 4.5))
79 + for i, (gid, label) in enumerate([("montreal", "Montréal"), ("quebec-city", "Québec"),
80 + ("gatineau", "Gatineau")]):
81 + s = cell(c, gid, "condo")
82 + ax.plot(dts(s["week"]), s["index_smoothed"], color=P.SERIES[i], label=label)
83 + ax.set_title("Condo indexes by city (2021 = 100)")
84 + ax.legend(loc="upper left")
85 + P.save(fig, FIGURES_DIR / "condo_by_city.png")
86 +
87 + # 5 — Raw median vs hedonic (province, unifamilial)
88 + bi = pl.read_parquet("data/interim/baseline_indexes.parquet")
89 + b = bi.filter((pl.col("geography_id") == "quebec")
90 + & (pl.col("property_type") == "unifamilial")).sort("week")
91 + med = b["raw_median"].to_numpy()
92 + base_med = np.nanmean(med[:52])
93 + fig, ax = plt.subplots(figsize=(9, 4.5))
94 + ax.plot(dts(b["week"]), med / base_med * 100, color=P.SERIES[1], lw=1.2,
95 + label="Raw weekly median (indexed)")
96 + h = cell(c, "quebec", "unifamilial")
97 + ax.plot(dts(h["week"]), h["index_smoothed"], color=P.SERIES[0],
98 + label="QWHPI (quality-adjusted)")
99 + ax.set_title("Composition noise: raw median vs hedonic index — Quebec unifamilial")
100 + ax.legend(loc="upper left")
101 + P.save(fig, FIGURES_DIR / "raw_median_vs_hedonic.png")
102 +
103 + # 6 — Weekly volumes
104 + vol = pl.read_parquet(PROCESSED_DIR / "weekly_liquidity.parquet")
105 + v = vol.filter((pl.col("geography_id") == "quebec")
106 + & (pl.col("property_type") == "all")).sort("week")
107 + fig, ax = plt.subplots(figsize=(9, 3.2))
108 + ax.bar(dts(v["week"]), v["transactions"], width=6, color=P.SERIES[0], alpha=0.6)
109 + ax.set_title("Weekly transaction volume — Quebec (research sample)")
110 + P.save(fig, FIGURES_DIR / "volumes.png")
111 +
112 + # 7 — Downsampling stability
113 + ds = pl.read_csv(TABLES_DIR / "downsampling_results.csv")
114 + fig, ax = plt.subplots(figsize=(7, 4))
115 + ax.plot(ds["target_tx_per_week"], ds["rmse_log_pct"], "o-", color=P.SERIES[0],
116 + label="RMSE vs full-sample path (%)")
117 + ax.plot(ds["target_tx_per_week"], (1 - ds["ci95_coverage"]) * 10, "s--",
118 + color=P.SERIES[1], label="(1 − CI coverage) × 10")
119 + ax.set_xlabel("target transactions per week")
120 + ax.set_title("Downsampling experiment — Montréal condo")
121 + ax.legend()
122 + ax.invert_xaxis()
123 + P.save(fig, FIGURES_DIR / "downsampling_stability.png")
124 +
125 + # 8 — YoY map (choropleth)
126 + import geopandas as gpd
127 + latest = c.filter((pl.col("geography_level") == "region")
128 + & (pl.col("property_type") == "all")
129 + & (~pl.col("is_partial_week")))
130 + lw = latest.filter(pl.col("week") == latest["week"].max())
131 + g = gpd.read_file("web/public/regions.geojson")
132 + g = g.merge(lw.select("geography_id", "yoy_pct").to_pandas(), on="geography_id")
133 + fig, ax = plt.subplots(figsize=(8, 7))
134 + g.plot(column="yoy_pct", cmap="Blues", legend=True, ax=ax,
135 + edgecolor="#fcfcfb", linewidth=0.8,
136 + legend_kwds={"label": "YoY %", "shrink": 0.6})
137 + ax.set_axis_off()
138 + ax.set_title(f"Year-over-year appreciation by region — week of {lw['week'][0]}")
139 + P.save(fig, FIGURES_DIR / "yoy_map.png")
140 +
141 + # 9 — Assessment gap evolution
142 + gap = pl.read_parquet(PROCESSED_DIR / "assessment_gap.parquet")
143 + fig, ax = plt.subplots(figsize=(9, 4))
144 + for i, t in enumerate(["unifamilial", "condo", "plex"]):
145 + s = gap.filter((pl.col("geography_id") == "quebec")
146 + & (pl.col("property_type") == t)).sort("week")
147 + # 13-week rolling median for readability
148 + vals = s["median_ratio"].rolling_median(window_size=13)
149 + ax.plot(dts(s["week"]), vals, color=P.SERIES[i], label=t)
150 + ax.axhline(1.0, color=P.TEXT2, lw=1, ls="--")
151 + ax.set_title("Assessment gap — median sale price / municipal assessment (13-wk median)")
152 + ax.legend(loc="upper left")
153 + P.save(fig, FIGURES_DIR / "assessment_gap.png")
154 +
155 + # 10 — Reliability heat table (cells × grade)
156 + summary = (
157 + c.group_by(["geography_id", "property_type"])
158 + .agg(pl.col("reliability_grade").mode().first().alias("grade"))
159 + )
160 + counts = summary.group_by(["property_type", "grade"]).len().sort(["property_type", "grade"])
161 + fig, ax = plt.subplots(figsize=(7, 3.4))
162 + grades = ["A", "B", "C", "D", "E"]
163 + types = ["all", "unifamilial", "condo", "plex"]
164 + mat = np.zeros((len(types), len(grades)))
165 + for r in counts.iter_rows(named=True):
166 + mat[types.index(r["property_type"]), grades.index(r["grade"])] = r["len"]
167 + im = ax.imshow(mat, cmap="Blues", aspect="auto")
168 + ax.set_xticks(range(len(grades)), grades)
169 + ax.set_yticks(range(len(types)), types)
170 + for i in range(len(types)):
171 + for j in range(len(grades)):
172 + if mat[i, j]:
173 + ax.text(j, i, int(mat[i, j]), ha="center", va="center",
174 + color=P.TEXT if mat[i, j] < mat.max() * 0.6 else "white")
175 + ax.set_title("Published series by reliability grade")
176 + ax.grid(False)
177 + P.save(fig, FIGURES_DIR / "reliability_matrix.png")
178 +
179 + print("All figures written to outputs/figures/")
180 +
181 +
182 +if __name__ == "__main__":
183 + main()
added engine/scripts/09_monthly.py +275 −0
@@ -0,0 +1,275 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/09_monthly.py
7 +# Purpose : v2.1 — monthly hybrid indexes: RTD province anchor, DIRECT robust
8 +# local time-dummy for liquid cells, hierarchical shrinkage for
9 +# thin cells only.
10 +# =============================================================================
11 +"""Monthly index build (v2.1 headline).
12 +
13 +Architecture (arbitrated against repeat sales, local time dummies and
14 +stratified matched-cell medians — see methodology_v2_research.md):
15 +
16 +- Province × type ....... rolling-time-dummy, Huber IRLS, mean splice.
17 +- Liquid cell (median ≥ LIQUID_MIN tx/month) ... DIRECT local robust
18 + time-dummy (own β, own FSA FE), then a light state-space smoother
19 + (obs var σ²/nₘ) for the real-time variant. Deviation-from-pooled-surface
20 + methods compress strong local divergences (Québec City condo: +50–55%
21 + by every composition-free arbiter vs +42% by deviations), so liquid
22 + cells are estimated on their own data.
23 +- Thin cell ............. deviation from its parent's published path via the
24 + heteroskedastic local-level Kalman model (unchanged machinery).
25 +"""
26 +
27 +from __future__ import annotations
28 +
29 +import sys
30 +import time
31 +import unicodedata
32 +from pathlib import Path
33 +
34 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
35 +
36 +import numpy as np
37 +import polars as pl
38 +
39 +from qwhpi.clean import CLEAN_PARQUET, research_sample
40 +from qwhpi.config import CITY_TARGETS, INTERIM_DIR, PROPERTY_TYPES, TABLES_DIR, ensure_dirs
41 +from qwhpi.features import build_features
42 +from qwhpi.hierarchy import CellDeviation, base_constant, cell_deviation, compose_cell_table
43 +from qwhpi.rtd import fullpool_residualize, local_time_dummy, run_rtd
44 +from qwhpi.state_space import fit_local_level
45 +
46 +MONTHLY_PARQUET = INTERIM_DIR / "monthly_indexes.parquet"
47 +FP_RESID = INTERIM_DIR / "rtd_residuals.parquet"
48 +RTD_PATHS = INTERIM_DIR / "rtd_paths.parquet"
49 +
50 +LIQUID_MIN = 40 # median tx/month for direct local estimation
51 +
52 +
53 +def slug(name: str) -> str:
54 + s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
55 + return s.lower().replace(" ", "-").replace("'", "")
56 +
57 +
58 +def city_slug(name: str) -> str:
59 + return "quebec-city" if name == "Québec" else slug(name)
60 +
61 +
62 +def compose_direct(grid, raw_path, n, sigma2, gid, gname, glevel, ptype):
63 + """Published table for a directly-estimated liquid cell."""
64 + fit = fit_local_level(raw_path, np.maximum(n, 1.0), sigma2)
65 + base = base_constant(grid, fit.filtered)
66 + log_raw = raw_path - base
67 + log_filt = fit.filtered - base
68 + log_smooth = fit.smoothed - base
69 + se = np.sqrt(np.maximum(fit.filtered_var, 0.0))
70 + return pl.DataFrame({
71 + "week": grid,
72 + "geography_id": gid, "geography_name": gname, "geography_level": glevel,
73 + "property_type": ptype,
74 + "index": np.exp(log_raw) * 100.0,
75 + "index_smoothed": np.exp(log_filt) * 100.0,
76 + "index_research": np.exp(log_smooth) * 100.0,
77 + "log_index": log_filt,
78 + "se_log": se,
79 + "lower_95": np.exp(log_filt - 1.96 * se) * 100.0,
80 + "upper_95": np.exp(log_filt + 1.96 * se) * 100.0,
81 + "transactions": n,
82 + "shrinkage_weight": 1.0 - fit.gain, # temporal smoothing weight
83 + "research_var": np.maximum(fit.smoothed_var, 0.0),
84 + }), fit
85 +
86 +
87 +def combine_all(tables, weights, gid, gname, glevel):
88 + """Fixed-share composition of the three type indexes into 'all'."""
89 + wsum = sum(weights.values())
90 + w = {k: v / wsum for k, v in weights.items()}
91 + base = tables[next(iter(tables))].sort("week")
92 + grid = base["week"].to_list()
93 +
94 + def wavg(col, log=True):
95 + acc = np.zeros(len(grid))
96 + for k, tbl in tables.items():
97 + vals = tbl.sort("week")[col].to_numpy()
98 + acc += w[k] * (np.log(vals / 100.0) if log else vals)
99 + return acc
100 +
101 + log_raw, log_filt, log_res = wavg("index"), wavg("index_smoothed"), wavg("index_research")
102 + var = sum((w[k] ** 2) * (tbl.sort("week")["se_log"].to_numpy() ** 2)
103 + for k, tbl in tables.items())
104 + se = np.sqrt(var)
105 + n = sum(np.nan_to_num(tbl.sort("week")["transactions"].to_numpy())
106 + for tbl in tables.values())
107 + return pl.DataFrame({
108 + "week": grid,
109 + "geography_id": gid, "geography_name": gname, "geography_level": glevel,
110 + "property_type": "all",
111 + "index": np.exp(log_raw) * 100.0,
112 + "index_smoothed": np.exp(log_filt) * 100.0,
113 + "index_research": np.exp(log_res) * 100.0,
114 + "log_index": log_filt,
115 + "se_log": se,
116 + "lower_95": np.exp(log_filt - 1.96 * se) * 100.0,
117 + "upper_95": np.exp(log_filt + 1.96 * se) * 100.0,
118 + "transactions": n,
119 + "shrinkage_weight": wavg("shrinkage_weight", log=False),
120 + "research_var": var,
121 + })
122 +
123 +
124 +def main() -> None:
125 + ensure_dirs()
126 + feat = build_features(research_sample(pl.read_parquet(CLEAN_PARQUET)))
127 +
128 + print("Stage 1: province RTD (13-month windows, Huber IRLS) ...")
129 + t0 = time.time()
130 + rtd = run_rtd(feat, verbose=False)
131 + print(f" done in {time.time() - t0:.0f}s")
132 + rtd.drift.write_csv(TABLES_DIR / "rtd_coefficient_drift.csv")
133 +
134 + print("Stage 1b: full-pool robust residualization (thin-cell hierarchy) ...")
135 + fp_resid = fullpool_residualize(feat)
136 + fp_resid.write_parquet(FP_RESID)
137 + sigma2 = {t: float(fp_resid.filter(pl.col("propertyType") == t)["resid"].var())
138 + for t in PROPERTY_TYPES}
139 +
140 + grid = rtd.months
141 + T = len(grid)
142 + pl.concat([
143 + pl.DataFrame({"month": grid, "property_type": t,
144 + "mean_splice": rtd.paths[t],
145 + "movement_splice": rtd.paths_movement[t],
146 + "window_splice": rtd.paths_window[t]})
147 + for t in PROPERTY_TYPES
148 + ]).write_parquet(RTD_PATHS)
149 +
150 + resid = fp_resid.rename({"month": "week_str"})
151 + region_names = dict(feat.select("region_code", "region").unique().iter_rows())
152 + frames: list[pl.DataFrame] = []
153 + estimation_log: list[dict] = []
154 +
155 + # Parent references for thin cells:
156 + region_resid_dev: dict[tuple[str, str], CellDeviation] = {}
157 + region_anchor: dict[tuple[str, str], np.ndarray] = {} # published filtered log path
158 +
159 + for ptype in PROPERTY_TYPES:
160 + path = rtd.paths[ptype]
161 + r_t = resid.filter(pl.col("propertyType") == ptype)
162 + f_t = feat.filter(pl.col("propertyType") == ptype)
163 + counts = {r["week_str"]: r["n"] for r in
164 + r_t.group_by("week_str").agg(pl.len().alias("n")).iter_rows(named=True)}
165 + n_prov = np.array([counts.get(m, 0) for m in grid], dtype=float)
166 + path_var = np.where(n_prov > 0, sigma2[ptype] / np.maximum(n_prov, 1), np.nan)
167 + path_var = np.nan_to_num(path_var, nan=float(np.nanmax(path_var)))
168 +
169 + prov = compose_cell_table(
170 + grid=grid, province_path=path, deviations=[],
171 + geography_id="quebec", geography_name="Québec (province)",
172 + geography_level="province", property_type=ptype, path_var=path_var,
173 + ).with_columns(pl.Series("transactions", n_prov),
174 + pl.Series("shrinkage_weight", np.zeros(T)))
175 + frames.append(prov)
176 + estimation_log.append({"cell": f"quebec/{ptype}", "method": "rtd_mean_splice"})
177 +
178 + def monthly_counts(sub: pl.DataFrame) -> np.ndarray:
179 + c = dict(sub.group_by("month").agg(pl.len().alias("n")).iter_rows())
180 + return np.array([c.get(m, 0) for m in grid], dtype=float)
181 +
182 + # ---------------- regions ---------------- #
183 + for rcode in sorted(region_names):
184 + cell_r = r_t.filter(pl.col("region_code") == rcode)
185 + cell_f = f_t.filter(pl.col("region_code") == rcode)
186 + n_m = monthly_counts(cell_f)
187 + med = float(np.median(n_m))
188 + rdev = cell_deviation(cell_r.rename({"resid": "dev"})
189 + .select("week_str", "dev"), grid, sigma2[ptype])
190 + region_resid_dev[(rcode, ptype)] = rdev
191 + gid = f"region-{rcode}"
192 +
193 + direct = local_time_dummy(cell_f, grid) if med >= LIQUID_MIN else None
194 + if direct is not None:
195 + raw_path, n_cell, s2 = direct
196 + tbl, fit = compose_direct(grid, raw_path, n_cell, s2,
197 + gid, region_names[rcode], "region", ptype)
198 + frames.append(tbl)
199 + region_anchor[(rcode, ptype)] = raw_path * 0 + fit.filtered # filtered log path
200 + estimation_log.append({"cell": f"{gid}/{ptype}", "method": "direct_local_td",
201 + "median_tx": med})
202 + else:
203 + tbl = compose_cell_table(
204 + grid=grid, province_path=path, deviations=[rdev],
205 + geography_id=gid, geography_name=region_names[rcode],
206 + geography_level="region", property_type=ptype, path_var=path_var)
207 + frames.append(tbl)
208 + region_anchor[(rcode, ptype)] = path + rdev.fit.filtered
209 + estimation_log.append({"cell": f"{gid}/{ptype}", "method": "hierarchical",
210 + "median_tx": med})
211 +
212 + # ---------------- cities ---------------- #
213 + for city in CITY_TARGETS:
214 + cell_f = f_t.filter(pl.col("municipality") == city)
215 + if cell_f.height == 0:
216 + continue
217 + rcode = cell_f["region_code"][0]
218 + gid = city_slug(city)
219 + n_m = monthly_counts(cell_f)
220 + med = float(np.median(n_m))
221 +
222 + direct = local_time_dummy(cell_f, grid) if med >= LIQUID_MIN else None
223 + if direct is not None:
224 + raw_path, n_cell, s2 = direct
225 + tbl, _ = compose_direct(grid, raw_path, n_cell, s2,
226 + gid, city, "municipality", ptype)
227 + frames.append(tbl)
228 + estimation_log.append({"cell": f"{gid}/{ptype}", "method": "direct_local_td",
229 + "median_tx": med})
230 + else:
231 + # deviation of the city vs its REGION's residual path, anchored
232 + # on the region's published (filtered) log path
233 + rdev = region_resid_dev[(rcode, ptype)]
234 + rpath = {m: v for m, v in zip(grid, rdev.fit.smoothed)}
235 + cell_r = r_t.filter(pl.col("municipality") == city).with_columns(
236 + (pl.col("resid") - pl.col("week_str").replace_strict(rpath, default=0.0))
237 + .alias("dev"))
238 + mdev = cell_deviation(cell_r.select("week_str", "dev"), grid, sigma2[ptype])
239 + tbl = compose_cell_table(
240 + grid=grid, province_path=region_anchor[(rcode, ptype)],
241 + deviations=[mdev],
242 + geography_id=gid, geography_name=city,
243 + geography_level="municipality", property_type=ptype,
244 + path_var=path_var)
245 + frames.append(tbl)
246 + estimation_log.append({"cell": f"{gid}/{ptype}", "method": "hierarchical",
247 + "median_tx": med})
248 +
249 + out = pl.concat(frames)
250 +
251 + all_frames = []
252 + for gid, gname, glevel in out.select(
253 + "geography_id", "geography_name", "geography_level").unique().iter_rows():
254 + tables = {t: out.filter((pl.col("geography_id") == gid)
255 + & (pl.col("property_type") == t))
256 + for t in PROPERTY_TYPES}
257 + tables = {t: tbl for t, tbl in tables.items() if tbl.height}
258 + weights = {t: max(float(np.nansum(tbl["transactions"].to_numpy())), 1.0)
259 + for t, tbl in tables.items()}
260 + all_frames.append(combine_all(tables, weights, gid, gname, glevel))
261 +
262 + out = pl.concat([out, *all_frames]).rename({"week": "period"}).sort(
263 + ["geography_level", "geography_id", "property_type", "period"])
264 + out.write_parquet(MONTHLY_PARQUET)
265 +
266 + log_df = pl.DataFrame(estimation_log)
267 + log_df.write_csv(TABLES_DIR / "estimation_methods.csv")
268 + n_direct = log_df.filter(pl.col("method") == "direct_local_td").height
269 + n_hier = log_df.filter(pl.col("method") == "hierarchical").height
270 + print(f"\nCells: {n_direct} direct, {n_hier} hierarchical, 3 RTD province")
271 + print(f"Saved {out.height:,} observations to {MONTHLY_PARQUET}")
272 +
273 +
274 +if __name__ == "__main__":
275 + main()
added engine/scripts/10_monthly_validation.py +219 −0
@@ -0,0 +1,219 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/10_monthly_validation.py
7 +# Purpose : v2.1 validation — repeat sales, downsampling (direct vs
8 +# hierarchical threshold), composition shock, drift & seasonality.
9 +# =============================================================================
10 +"""Monthly validation suite (v2.1)."""
11 +
12 +from __future__ import annotations
13 +
14 +import sys
15 +from pathlib import Path
16 +
17 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
18 +
19 +import numpy as np
20 +import polars as pl
21 +
22 +from qwhpi.clean import CLEAN_PARQUET, research_sample
23 +from qwhpi.config import INTERIM_DIR, REPORTS_DIR, TABLES_DIR, ensure_dirs
24 +from qwhpi.features import build_features
25 +from qwhpi.hierarchy import cell_deviation
26 +from qwhpi.repeat_sales import bmn_index, build_pairs
27 +from qwhpi.rtd import local_time_dummy, month_grid
28 +from qwhpi.seasonal import seasonality_test
29 +from qwhpi.state_space import fit_local_level
30 +
31 +RNG = np.random.default_rng(42)
32 +LINES: list[str] = []
33 +
34 +
35 +def note(line: str = "") -> None:
36 + LINES.append(line)
37 + print(line)
38 +
39 +
40 +def main() -> None:
41 + ensure_dirs()
42 + cl = research_sample(pl.read_parquet(CLEAN_PARQUET))
43 + feat = build_features(cl)
44 + monthly = pl.read_parquet(INTERIM_DIR / "monthly_indexes.parquet")
45 + fp = pl.read_parquet(INTERIM_DIR / "rtd_residuals.parquet")
46 + grid = month_grid(sorted(fp["month"].unique().to_list()))
47 +
48 + note("# QHPI v2.1 validation report (monthly, robust)")
49 + note("")
50 +
51 + # ------------------------------------------------------------------ #
52 + # 1. Repeat-sales comparison
53 + # ------------------------------------------------------------------ #
54 + note("## 1. Repeat sales (BMN, unit-signature pairs) vs published index")
55 + note("")
56 + rows = []
57 + for gid, mun, ptype in [
58 + ("quebec", None, "unifamilial"), ("quebec", None, "condo"),
59 + ("quebec", None, "plex"), ("montreal", "Montréal", "condo"),
60 + ("montreal", "Montréal", "unifamilial"),
61 + ("quebec-city", "Québec", "condo"),
62 + ("quebec-city", "Québec", "unifamilial"),
63 + ]:
64 + sub = cl.filter(pl.col("propertyType") == ptype)
65 + if mun:
66 + sub = sub.filter(pl.col("municipality") == mun)
67 + rs = bmn_index(build_pairs(sub))
68 + if rs is None:
69 + continue
70 + h = monthly.filter((pl.col("geography_id") == gid)
71 + & (pl.col("property_type") == ptype)) \
72 + .select(pl.col("period").alias("month"), "index_research")
73 + j = rs.join(h, on="month", how="inner").sort("month")
74 + lg_rs = np.log(j["rs_index"].to_numpy())
75 + lg_he = np.log(j["index_research"].to_numpy())
76 + corr = float(np.corrcoef(np.diff(lg_rs), np.diff(lg_he))[0, 1])
77 + gap = float((lg_he[-2] - lg_rs[-2]) * 100)
78 + rows.append({"cell": f"{gid}/{ptype}", "n_pairs": build_pairs(sub).height,
79 + "corr_monthly_changes": round(corr, 3),
80 + "end_gap_pct": round(gap, 2)})
81 + note(f"- {gid}/{ptype}: Δcorr={corr:.3f}, écart final hedonic−RS = {gap:+.1f}%")
82 + pl.DataFrame(rows).write_csv(TABLES_DIR / "repeat_sales_comparison_monthly.csv")
83 +
84 + # ------------------------------------------------------------------ #
85 + # 2. Downsampling — validates LIQUID_MIN and the grade scale
86 + # ------------------------------------------------------------------ #
87 + note("")
88 + note("## 2. Downsampling (Montréal condo, ~620 tx/month)")
89 + note("")
90 + mc_feat = feat.filter((pl.col("municipality") == "Montréal")
91 + & (pl.col("propertyType") == "condo"))
92 + ref = local_time_dummy(mc_feat, grid)
93 + assert ref is not None
94 + ref_path = ref[0] - ref[0].mean()
95 +
96 + mc_resid = fp.filter((pl.col("municipality") == "Montréal")
97 + & (pl.col("propertyType") == "condo")) \
98 + .rename({"month": "week_str"})
99 + sig2 = float(mc_resid["resid"].var())
100 + prov_path_tbl = pl.read_parquet(INTERIM_DIR / "rtd_paths.parquet")
101 + prov = prov_path_tbl.filter(pl.col("property_type") == "condo").sort("month")
102 + prov_path = prov["mean_splice"].to_numpy()
103 +
104 + rows = []
105 + for target, n_reps in [(300, 8), (150, 8), (75, 8), (40, 10), (20, 10), (10, 10)]:
106 + rmse_d, rmse_h = [], []
107 + for _ in range(n_reps):
108 + thin_f = (mc_feat.with_columns(pl.lit(RNG.random(mc_feat.height)).alias("_u"))
109 + .with_columns(pl.col("_u").rank().over("month").alias("_rk"))
110 + .filter(pl.col("_rk") <= target))
111 + # direct estimator on thinned cell
112 + d = local_time_dummy(thin_f, grid)
113 + if d is not None:
114 + sm = fit_local_level(d[0], np.maximum(d[1], 1.0), d[2]).smoothed
115 + err = (sm - sm.mean()) - ref_path
116 + rmse_d.append(np.sqrt(np.mean(err ** 2)))
117 + # hierarchical estimator on thinned residuals
118 + thin_r = (mc_resid.with_columns(pl.lit(RNG.random(mc_resid.height)).alias("_u"))
119 + .with_columns(pl.col("_u").rank().over("week_str").alias("_rk"))
120 + .filter(pl.col("_rk") <= target))
121 + dev = cell_deviation(thin_r.rename({"resid": "dev"})
122 + .select("week_str", "dev"), grid, sig2)
123 + hier = prov_path + dev.fit.smoothed
124 + err = (hier - hier.mean()) - ref_path
125 + rmse_h.append(np.sqrt(np.mean(err ** 2)))
126 + rows.append({"target_tx_per_month": target,
127 + "rmse_direct_pct": round(float(np.mean(rmse_d)) * 100, 2) if rmse_d else None,
128 + "rmse_hierarchical_pct": round(float(np.mean(rmse_h)) * 100, 2)})
129 + note(f"- {target:>3} tx/mois: RMSE direct={rows[-1]['rmse_direct_pct']}% "
130 + f"| hiérarchique={rows[-1]['rmse_hierarchical_pct']}%")
131 + pl.DataFrame(rows).write_csv(TABLES_DIR / "downsampling_monthly.csv")
132 + note("")
133 + note("Lecture: en RMSE pur (référence = direct plein échantillon, cellule à "
134 + "faible divergence) le croisement se situe vers 75–150 tx/mois. Mais le "
135 + "RMSE du hiérarchique contient un biais de compression qui explose sur "
136 + "les cellules fortement divergentes (Québec-ville condo: ~8–10 pts, cf. "
137 + "arbitrage RS/stratifié) alors que l'erreur du direct est du bruit pur "
138 + "(~1.9% à 40 tx/mois), réduit par le lisseur d'état. LIQUID_MIN=40 "
139 + "échange donc un bruit borné contre un biais non borné.")
140 +
141 + # ------------------------------------------------------------------ #
142 + # 3. Composition shock (monthly)
143 + # ------------------------------------------------------------------ #
144 + note("")
145 + note("## 3. Choc de composition (province unifamilial, mensuel)")
146 + import pyfixest as pf
147 + uni = feat.filter(pl.col("propertyType") == "unifamilial")
148 + med_fa = float(uni["floor_area_filled"].median())
149 + shock_months = [m for m in grid if m.startswith("2024")][:6]
150 + u = RNG.random(uni.height)
151 + uni = uni.with_columns(pl.lit(u).alias("_u"))
152 + shocked = uni.filter(~(pl.col("month").is_in(shock_months)
153 + & (pl.col("floor_area_filled") < med_fa)
154 + & (pl.col("_u") < 0.7)))
155 +
156 + def med_path(df):
157 + return {r["month"]: r["m"] for r in
158 + df.group_by("month").agg(pl.col("amount").median().alias("m"))
159 + .iter_rows(named=True)}
160 +
161 + def hed_path(df):
162 + pdf = df.select("log_amount", "month", "log_fa", "fa_missing",
163 + "age_bin", "loc_fine").to_pandas()
164 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
165 + fit = pf.feols("log_amount ~ log_fa + fa_missing + C(age_bin) "
166 + "| loc_fine + month", data=pdf)
167 + fx = fit.fixef()
168 + key = next(k for k in fx if "month" in k)
169 + return {k: float(v) for k, v in fx[key].items()}
170 +
171 + mb, ms = med_path(uni), med_path(shocked)
172 + hb, hs = hed_path(uni), hed_path(shocked)
173 + common = [m for m in shock_months if m in mb and m in ms and m in hb and m in hs]
174 + dmed = float(np.mean([np.log(ms[m] / mb[m]) for m in common])) * 100
175 + dhed = float(np.mean([hs[m] - hb[m] for m in common])) * 100
176 + note(f"- Médiane brute: {dmed:+.2f}% dans les mois choqués (doit bouger)")
177 + note(f"- Hédonique: {dhed:+.2f}% (doit rester ~0)")
178 + pl.DataFrame({"metric": ["raw_median_shift_pct", "hedonic_shift_pct"],
179 + "value": [round(dmed, 3), round(dhed, 3)]}) \
180 + .write_csv(TABLES_DIR / "composition_shock_monthly.csv")
181 +
182 + # ------------------------------------------------------------------ #
183 + # 4. Drift, splice, seasonality
184 + # ------------------------------------------------------------------ #
185 + note("")
186 + note("## 4. Diagnostics de robustesse")
187 + drift = pl.read_csv(TABLES_DIR / "rtd_coefficient_drift.csv")
188 + b = drift["beta_log_fa"]
189 + note(f"- Dérive β(log surface) sur 55 fenêtres: {b.min():.3f}{b.max():.3f} "
190 + f"(amplitude {(b.max() - b.min()) / b.mean() * 100:.1f}% de la moyenne) — "
191 + "justifie le RTD contre le pool fixe.")
192 + seas_rows = []
193 + for gid, t in [("quebec", "all"), ("quebec", "unifamilial"), ("montreal", "condo")]:
194 + s = monthly.filter((pl.col("geography_id") == gid)
195 + & (pl.col("property_type") == t)).sort("period")
196 + res = seasonality_test(np.log(s["index_research"].to_numpy()), n_harmonics=2)
197 + seas_rows.append({"cell": f"{gid}/{t}", **res})
198 + note(f"- Saisonnalité {gid}/{t}: F={res['f_stat']} p={res['p_value']} → "
199 + + ("significative" if res["significant_5pct"] else "non significative (NSA)"))
200 + pl.DataFrame(seas_rows).write_csv(TABLES_DIR / "seasonality_monthly.csv")
201 +
202 + header = (
203 + "<!--\n"
204 + "=============================================================================\n"
205 + "QWHPI — Quebec Weekly Housing Price Index\n"
206 + "Author : Simon-Pierre Boucher\n"
207 + "Contact : contact@spboucher.ai\n"
208 + "File : outputs/reports/validation_v2_report.md\n"
209 + "Purpose : v2.1 monthly validation report (10_monthly_validation.py)\n"
210 + "=============================================================================\n"
211 + "-->\n\n"
212 + )
213 + (REPORTS_DIR / "validation_v2_report.md").write_text(
214 + header + "\n".join(LINES) + "\n", encoding="utf-8")
215 + print("\nReport: outputs/reports/validation_v2_report.md")
216 +
217 +
218 +if __name__ == "__main__":
219 + main()
added engine/scripts/11_monthly_canonical.py +220 −0
@@ -0,0 +1,220 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : engine/scripts/11_monthly_canonical.py
7 +# Purpose : v2.1 canonical dataset — qhpi_monthly.parquet with growth
8 +# horizons, reliability grades, dollar values, vintages, coverage.
9 +# =============================================================================
10 +"""Canonical monthly dataset build (v2.1).
11 +
12 +Reliability grades are anchored on the monthly downsampling experiment
13 +(outputs/tables/downsampling_monthly.csv): smoothed-path RMSE ≈ 1.0–1.2% at
14 +150+ tx/month, ≈1.5% at 75, ≈1.9% at 40, ≥2.6% at 10.
15 + A: median ≥ 150 tx/month and se_log < 1.5%
16 + B: median ≥ 75 and se_log < 2.5%
17 + C: median ≥ 40 (direct estimation floor)
18 + D: median ≥ 15 (hierarchical, heavy shrinkage)
19 + E: < 15 (model-implied, transparency only)
20 +"""
21 +
22 +from __future__ import annotations
23 +
24 +import datetime as dt
25 +import sys
26 +import unicodedata
27 +from pathlib import Path
28 +
29 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
30 +
31 +import numpy as np
32 +import polars as pl
33 +
34 +from qwhpi.clean import CLEAN_PARQUET, research_sample
35 +from qwhpi.config import (
36 + CITY_TARGETS, INTERIM_DIR, PROCESSED_DIR, PROPERTY_TYPES, TABLES_DIR,
37 + ensure_dirs,
38 +)
39 +
40 +MODEL_VERSION = "2.1.0"
41 +CANONICAL = PROCESSED_DIR / "qhpi_monthly.parquet"
42 +FIRST_RELEASE = PROCESSED_DIR / "first_release_monthly.parquet"
43 +VINTAGE_DIR = PROCESSED_DIR / "vintages_monthly"
44 +KEY = ["geography_id", "property_type", "period"]
45 +
46 +GROWTH = {"monthly_pct": 1, "three_month_pct": 3, "six_month_pct": 6, "yoy_pct": 12}
47 +
48 +
49 +def slug(name: str) -> str:
50 + s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
51 + return s.lower().replace(" ", "-").replace("'", "")
52 +
53 +
54 +def city_slug(name: str) -> str:
55 + return "quebec-city" if name == "Québec" else slug(name)
56 +
57 +
58 +def main() -> None:
59 + ensure_dirs()
60 + data_vintage = dt.date.today().isoformat()
61 + m = pl.read_parquet(INTERIM_DIR / "monthly_indexes.parquet")
62 + cl = research_sample(pl.read_parquet(CLEAN_PARQUET)).with_columns(
63 + pl.col("date").dt.strftime("%Y-%m").alias("period"))
64 +
65 + # ---------------- transactions per published cell ---------------- #
66 + cell_frames = [
67 + cl.select(pl.lit("quebec").alias("geography_id"),
68 + pl.col("propertyType").alias("property_type"),
69 + "period", "log_amount", "amount"),
70 + cl.filter(pl.col("region_code").is_not_null()).select(
71 + pl.concat_str([pl.lit("region-"), pl.col("region_code")]).alias("geography_id"),
72 + pl.col("propertyType").alias("property_type"),
73 + "period", "log_amount", "amount"),
74 + ]
75 + for city in CITY_TARGETS:
76 + sub = cl.filter(pl.col("municipality") == city)
77 + if sub.height:
78 + cell_frames.append(sub.select(
79 + pl.lit(city_slug(city)).alias("geography_id"),
80 + pl.col("propertyType").alias("property_type"),
81 + "period", "log_amount", "amount"))
82 + tx = pl.concat(cell_frames)
83 + tx_all = pl.concat([tx, tx.with_columns(pl.lit("all").alias("property_type"))])
84 +
85 + # ---------------- representative dollar value ---------------- #
86 + basket = (
87 + tx_all.join(m.select(*KEY, "index_smoothed"), on=KEY, how="inner")
88 + .group_by(["geography_id", "property_type"])
89 + .agg((pl.col("log_amount") - (pl.col("index_smoothed") / 100.0).log())
90 + .mean().exp().round(0).alias("basket_value"))
91 + )
92 + canon = m.join(basket, on=["geography_id", "property_type"], how="left") \
93 + .with_columns((pl.col("basket_value") * pl.col("index_smoothed") / 100.0)
94 + .round(0).alias("representative_value"))
95 +
96 + # ---------------- growth horizons ---------------- #
97 + keys = ["geography_id", "property_type"]
98 + canon = canon.sort([*keys, "period"]).with_columns([
99 + ((pl.col("index_smoothed") / pl.col("index_smoothed").shift(h).over(keys) - 1) * 100)
100 + .round(3).alias(name)
101 + for name, h in GROWTH.items()
102 + ])
103 +
104 + # ---------------- reliability grades ---------------- #
105 + med = canon.group_by(keys).agg(
106 + pl.col("transactions").fill_nan(None).median().alias("cell_median_tx"))
107 + canon = canon.join(med, on=keys, how="left")
108 + base_grade = (
109 + pl.when((pl.col("cell_median_tx") >= 150) & (pl.col("se_log") < 0.015)).then(pl.lit("A"))
110 + .when((pl.col("cell_median_tx") >= 75) & (pl.col("se_log") < 0.025)).then(pl.lit("B"))
111 + .when(pl.col("cell_median_tx") >= 40).then(pl.lit("C"))
112 + .when(pl.col("cell_median_tx") >= 15).then(pl.lit("D"))
113 + .otherwise(pl.lit("E"))
114 + )
115 + downgrade = {"A": "B", "B": "C", "C": "D", "D": "E", "E": "E"}
116 + canon = canon.with_columns(
117 + pl.when(pl.col("transactions").fill_nan(0) == 0)
118 + .then(base_grade.replace(downgrade)).otherwise(base_grade)
119 + .alias("reliability_grade"))
120 +
121 + # ---------------- partial month (registration lag) ---------------- #
122 + max_date = cl["date"].max()
123 + last_period = max_date.strftime("%Y-%m")
124 + month_end = (dt.date(max_date.year + (max_date.month == 12),
125 + max_date.month % 12 + 1, 1) - dt.timedelta(days=1))
126 + is_partial_last = max_date < month_end
127 + canon = canon.with_columns(
128 + ((pl.col("period") == last_period) & is_partial_last).alias("is_partial_month"))
129 +
130 + # ---------------- effective sample size ---------------- #
131 + fp = pl.read_parquet(INTERIM_DIR / "rtd_residuals.parquet")
132 + sigma2 = {t: float(fp.filter(pl.col("propertyType") == t)["resid"].var())
133 + for t in PROPERTY_TYPES}
134 + sigma2["all"] = float(np.mean(list(sigma2.values())))
135 + canon = canon.with_columns(
136 + pl.col("property_type").replace_strict(sigma2, default=None).alias("_s2")
137 + ).with_columns(
138 + (pl.col("_s2") / (pl.col("se_log") ** 2).clip(1e-9, None))
139 + .round(1).alias("effective_sample_size")
140 + ).drop("_s2")
141 +
142 + canon = canon.with_columns(
143 + pl.lit(MODEL_VERSION).alias("model_version"),
144 + pl.lit(data_vintage).alias("data_vintage"),
145 + pl.lit("monthly").alias("frequency"),
146 + pl.col("transactions").fill_nan(0).cast(pl.Int64),
147 + )
148 + cols = ["period", "geography_level", "geography_id", "geography_name",
149 + "property_type", "index", "index_smoothed", "representative_value",
150 + "transactions", "effective_sample_size",
151 + "monthly_pct", "three_month_pct", "six_month_pct", "yoy_pct",
152 + "lower_95", "upper_95", "reliability_grade", "shrinkage_weight",
153 + "is_partial_month", "model_version", "data_vintage"]
154 + canonical = canon.select(cols).sort(
155 + ["geography_level", "geography_id", "property_type", "period"])
156 + canonical.write_parquet(CANONICAL)
157 + print(f"Canonical: {canonical.height:,} rows -> {CANONICAL}")
158 +
159 + # ---------------- liquidity + coverage ---------------- #
160 + liq = (tx_all.group_by([*keys, "period"]).len().rename({"len": "transactions"})
161 + .sort([*keys, "period"]))
162 + liq.write_parquet(PROCESSED_DIR / "monthly_liquidity.parquet")
163 +
164 + months_total = cl["period"].n_unique()
165 + cov = (
166 + cl.group_by(["municipality", "geo_code", "region", "propertyType", "period"]).len()
167 + .group_by(["municipality", "geo_code", "region", "propertyType"])
168 + .agg(pl.col("len").median().alias("median_monthly_tx"),
169 + pl.col("len").sum().alias("total_tx"),
170 + (pl.len() / months_total * 100).round(1).alias("pct_months_active"))
171 + .with_columns(
172 + pl.when(pl.col("municipality").is_in(list(CITY_TARGETS)))
173 + .then(pl.lit("published"))
174 + .when((pl.col("median_monthly_tx") >= 15) & (pl.col("pct_months_active") >= 85))
175 + .then(pl.lit("conditional"))
176 + .otherwise(pl.lit("not_published"))
177 + .alias("coverage_status"))
178 + .sort("total_tx", descending=True)
179 + )
180 + cov.write_csv(TABLES_DIR / "coverage_monthly.csv")
181 + cov.write_parquet(PROCESSED_DIR / "coverage_matrix_monthly.parquet")
182 + print("Coverage:", dict(cov.group_by("coverage_status").len().iter_rows()))
183 +
184 + # ---------------- assessment gap (monthly) ---------------- #
185 + gap = cl.filter(pl.col("totalArValue") > 0).with_columns(
186 + (pl.col("amount") / pl.col("totalArValue")).alias("ratio"))
187 + gap_cells = pl.concat([
188 + gap.select(pl.lit("quebec").alias("geography_id"),
189 + pl.col("propertyType").alias("property_type"), "period", "ratio"),
190 + gap.select(pl.concat_str([pl.lit("region-"), pl.col("region_code")])
191 + .alias("geography_id"),
192 + pl.col("propertyType").alias("property_type"), "period", "ratio"),
193 + ])
194 + (gap_cells.group_by([*keys, "period"] if False else ["geography_id", "property_type", "period"])
195 + .agg(pl.col("ratio").median().round(4).alias("median_ratio"), pl.len().alias("n"))
196 + .sort(["geography_id", "property_type", "period"])
197 + .write_parquet(PROCESSED_DIR / "assessment_gap_monthly.parquet"))
198 +
199 + # ---------------- vintages (monthly store) ---------------- #
200 + vd = VINTAGE_DIR / f"data_vintage={data_vintage}"
201 + vd.mkdir(parents=True, exist_ok=True)
202 + canonical.write_parquet(vd / "qhpi_monthly.parquet")
203 + fresh = canonical.select(
204 + *KEY,
205 + pl.col("index").alias("first_release_index"),
206 + pl.col("index_smoothed").alias("first_release_index_smoothed"),
207 + pl.lit(data_vintage).alias("first_release_vintage"))
208 + if FIRST_RELEASE.exists():
209 + existing = pl.read_parquet(FIRST_RELEASE)
210 + fresh = pl.concat([existing, fresh.join(existing.select(KEY), on=KEY, how="anti")])
211 + fresh.write_parquet(FIRST_RELEASE)
212 +
213 + latest_period = canonical.filter(~pl.col("is_partial_month"))["period"].max()
214 + canonical.filter(pl.col("period") == latest_period) \
215 + .write_csv(TABLES_DIR / "index_latest_monthly.csv")
216 + print(f"Latest complete month: {latest_period}")
217 +
218 +
219 +if __name__ == "__main__":
220 + main()
added engine/src/qwhpi/__init__.py +12 −0
@@ -0,0 +1,12 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/__init__.py
6 +# Purpose : Package root for the QWHPI index engine.
7 +# =============================================================================
8 +"""QWHPI index engine — quality-adjusted weekly housing price indexes for Quebec."""
9 +
10 +__version__ = "0.1.0"
11 +__author__ = "Simon-Pierre Boucher"
12 +__contact__ = "contact@spboucher.ai"
added engine/src/qwhpi/clean.py +149 −0
@@ -0,0 +1,149 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/clean.py
6 +# Purpose : Reproducible cleaning — duplicate flags, arm's-length screens,
7 +# characteristic sanitation. Rows are flagged, never deleted.
8 +# =============================================================================
9 +"""Cleaning pipeline (Execution Order step 3).
10 +
11 +Principles (CLAUDE.md §4.3):
12 +- The raw CSV is immutable; this module only adds columns.
13 +- Nothing is silently deleted: every excluded row carries ``exclude_reason``.
14 +- Filters are economically justified and fully tabulated in the exclusion
15 + table (``outputs/tables/exclusions.csv``, written by scripts/03_clean.py).
16 +
17 +Screens applied (headline research sample):
18 +
19 +1. ``dup_exact`` — exact duplicate rows ignoring ``id`` (recording
20 + artifacts); first occurrence kept.
21 +2. ``dup_addr_date_amt`` — same street+city+date+amount (same conveyance
22 + registered twice); first occurrence kept. Same address+date with
23 + *different* amounts is legitimate (multi-unit condo/plex closings) and
24 + is NOT excluded.
25 +3. ``nonmarket_ratio`` — amount / totalArValue outside [0.2, 5.0] where the
26 + assessment is available. Transfers far below assessed value are
27 + predominantly non-arm's-length (family transfers, estate settlements,
28 + partial-interest deeds); far above, land assembly / data errors.
29 + Missing assessments are NOT excluded (no ratio evidence).
30 +4. ``price_ceiling`` — amount > $10M: portfolio, land-assembly or
31 + commercial-scale conveyances, not single-dwelling market sales.
32 +
33 +Note: the provider already floors prices at $50,000 — no additional low-price
34 +screen is required (documented, verified in the audit).
35 +
36 +Characteristic sanitation (set to null + flag, transaction retained):
37 +- ``floorArea`` outside [20, 2000] m² for a dwelling -> null (``fa_suspect``)
38 +- ``yearBuilt`` > sale year + 1 -> null (``yb_suspect``)
39 +
40 +``indéterminé`` property type is retained in the clean table but flagged;
41 +it never enters type-specific or "all" indexes (aggregate of the three
42 +determined types).
43 +"""
44 +
45 +from __future__ import annotations
46 +
47 +import polars as pl
48 +
49 +from qwhpi.config import PROCESSED_DIR, PROPERTY_TYPE_UNDETERMINED
50 +
51 +CLEAN_PARQUET = PROCESSED_DIR / "transactions_clean.parquet"
52 +
53 +RATIO_LO = 0.2
54 +RATIO_HI = 5.0
55 +PRICE_CEILING = 10_000_000
56 +FLOORAREA_LO = 20.0
57 +FLOORAREA_HI = 2000.0
58 +
59 +
60 +def add_flags(tx: pl.DataFrame, geo: pl.DataFrame) -> pl.DataFrame:
61 + """Merge geography and add duplicate/exclusion/sanitation flags."""
62 + df = tx.join(geo, on="id", how="left")
63 +
64 + # --- duplicates (order by id for a deterministic 'first occurrence') --- #
65 + content_cols = [c for c in tx.columns if c not in ("id",)]
66 + df = df.sort("id").with_columns(
67 + pl.int_range(pl.len()).over(content_cols).alias("_rk_exact"),
68 + pl.int_range(pl.len()).over(["street", "city", "date", "amount"]).alias("_rk_ada"),
69 + ).with_columns(
70 + (pl.col("_rk_exact") > 0).alias("dup_exact"),
71 + (
72 + (pl.col("_rk_ada") > 0)
73 + & pl.col("street").is_not_null()
74 + ).alias("dup_addr_date_amt"),
75 + ).drop("_rk_exact", "_rk_ada")
76 +
77 + df = df.with_columns(
78 + pl.when(pl.col("dup_exact"))
79 + .then(pl.lit("exact_duplicate"))
80 + .when(pl.col("dup_addr_date_amt"))
81 + .then(pl.lit("addr_date_amount_duplicate"))
82 + .otherwise(None)
83 + .alias("duplicate_reason"),
84 + (pl.col("dup_exact") | pl.col("dup_addr_date_amt")).alias("duplicate_flag"),
85 + )
86 +
87 + # --- arm's-length / plausibility screens --- #
88 + ratio = pl.col("amount") / pl.col("totalArValue")
89 + df = df.with_columns(
90 + pl.when(pl.col("totalArValue") > 0)
91 + .then(ratio)
92 + .otherwise(None)
93 + .alias("assessment_ratio"),
94 + ).with_columns(
95 + (
96 + pl.col("assessment_ratio").is_not_null()
97 + & ((pl.col("assessment_ratio") < RATIO_LO) | (pl.col("assessment_ratio") > RATIO_HI))
98 + ).alias("nonmarket_ratio"),
99 + (pl.col("amount") > PRICE_CEILING).alias("price_ceiling"),
100 + (pl.col("propertyType") == PROPERTY_TYPE_UNDETERMINED).alias("is_undetermined_type"),
101 + )
102 +
103 + # --- characteristic sanitation (null + flag, never drop the row) --- #
104 + sale_year = pl.col("date").dt.year()
105 + df = df.with_columns(
106 + (
107 + pl.col("floorArea").is_not_null()
108 + & ((pl.col("floorArea") < FLOORAREA_LO) | (pl.col("floorArea") > FLOORAREA_HI))
109 + ).alias("fa_suspect"),
110 + (
111 + pl.col("yearBuilt").is_not_null() & (pl.col("yearBuilt") > sale_year + 1)
112 + ).alias("yb_suspect"),
113 + ).with_columns(
114 + pl.when(pl.col("fa_suspect")).then(None).otherwise(pl.col("floorArea")).alias("floorArea"),
115 + pl.when(pl.col("yb_suspect")).then(None).otherwise(pl.col("yearBuilt")).alias("yearBuilt"),
116 + )
117 +
118 + # --- consolidated exclusion verdict (priority order for reporting) --- #
119 + df = df.with_columns(
120 + pl.when(pl.col("dup_exact")).then(pl.lit("exact_duplicate"))
121 + .when(pl.col("dup_addr_date_amt")).then(pl.lit("addr_date_amount_duplicate"))
122 + .when(pl.col("price_ceiling")).then(pl.lit("price_above_10m"))
123 + .when(pl.col("nonmarket_ratio")).then(pl.lit("assessment_ratio_outside_band"))
124 + .otherwise(None)
125 + .alias("exclude_reason")
126 + ).with_columns(
127 + pl.col("exclude_reason").is_null().alias("in_research_sample")
128 + )
129 + return df
130 +
131 +
132 +def research_sample(df: pl.DataFrame, include_undetermined: bool = False) -> pl.DataFrame:
133 + """Rows eligible for index estimation."""
134 + out = df.filter(pl.col("in_research_sample"))
135 + if not include_undetermined:
136 + out = out.filter(~pl.col("is_undetermined_type"))
137 + return out
138 +
139 +
140 +def build_clean(tx: pl.DataFrame, geo: pl.DataFrame, force: bool = False) -> pl.DataFrame:
141 + """Compute (or load cached) flagged clean table."""
142 + if CLEAN_PARQUET.exists() and not force:
143 + cached = pl.read_parquet(CLEAN_PARQUET)
144 + if cached.height == tx.height:
145 + return cached
146 + df = add_flags(tx, geo)
147 + PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
148 + df.write_parquet(CLEAN_PARQUET)
149 + return df
added engine/src/qwhpi/config.py +100 −0
@@ -0,0 +1,100 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/config.py
6 +# Purpose : Central configuration — paths, calendar, property types, constants.
7 +# =============================================================================
8 +"""Central configuration for the QWHPI engine.
9 +
10 +All paths are resolved relative to the repository root so every script and
11 +module agrees on where data lives, regardless of the working directory.
12 +"""
13 +
14 +from __future__ import annotations
15 +
16 +from dataclasses import dataclass, field
17 +from pathlib import Path
18 +
19 +# --------------------------------------------------------------------------- #
20 +# Paths
21 +# --------------------------------------------------------------------------- #
22 +
23 +REPO_ROOT = Path(__file__).resolve().parents[3]
24 +
25 +DATA_DIR = REPO_ROOT / "data"
26 +RAW_DIR = DATA_DIR / "raw"
27 +EXTERNAL_DIR = DATA_DIR / "external"
28 +INTERIM_DIR = DATA_DIR / "interim"
29 +PROCESSED_DIR = DATA_DIR / "processed"
30 +
31 +OUTPUTS_DIR = REPO_ROOT / "outputs"
32 +FIGURES_DIR = OUTPUTS_DIR / "figures"
33 +TABLES_DIR = OUTPUTS_DIR / "tables"
34 +MAPS_DIR = OUTPUTS_DIR / "maps"
35 +REPORTS_DIR = OUTPUTS_DIR / "reports"
36 +
37 +RAW_TRANSACTIONS_CSV = RAW_DIR / "province_transactions.csv"
38 +
39 +# --------------------------------------------------------------------------- #
40 +# Weekly calendar
41 +# --------------------------------------------------------------------------- #
42 +# Weeks run Monday -> Sunday and are labeled by their Monday. The grid is
43 +# continuous from FIRST_WEEK to the latest observed week; zero-transaction
44 +# weeks are kept and flagged, never dropped.
45 +
46 +FIRST_WEEK = "2021-01-04" # first Monday of the sample
47 +
48 +# --------------------------------------------------------------------------- #
49 +# Property types
50 +# --------------------------------------------------------------------------- #
51 +# `indeterminé` must never contaminate type-specific indexes; it is studied
52 +# separately and documented in an appendix.
53 +
54 +PROPERTY_TYPES = ("unifamilial", "condo", "plex")
55 +PROPERTY_TYPE_UNDETERMINED = "indéterminé"
56 +PROPERTY_TYPE_ALL = "all" # aggregate over the three determined types
57 +
58 +# --------------------------------------------------------------------------- #
59 +# Index conventions
60 +# --------------------------------------------------------------------------- #
61 +
62 +BASE_PERIOD_LABEL = "2021 average = 100"
63 +BASE_YEAR = 2021
64 +
65 +# Liquidity-tier starting hypotheses (to be validated by downsampling, §4.7).
66 +LIQUIDITY_TIERS = {
67 + "very_strong": 50, # >= 50 tx/week
68 + "strong": 20, # 20-49
69 + "shrinkage": 10, # 10-19
70 + "heavy_shrinkage": 5, # 5-9; below 5 -> model-implied
71 +}
72 +
73 +# QWHPI-CITY target municipalities (published as liquidity permits).
74 +CITY_TARGETS = (
75 + "Montréal", "Québec", "Laval", "Gatineau", "Longueuil",
76 + "Sherbrooke", "Trois-Rivières", "Saguenay", "Lévis", "Drummondville",
77 +)
78 +
79 +AUTHOR = "Simon-Pierre Boucher"
80 +CONTACT = "contact@spboucher.ai"
81 +
82 +
83 +@dataclass(frozen=True)
84 +class EngineConfig:
85 + """Runtime configuration for a pipeline execution."""
86 +
87 + raw_csv: Path = RAW_TRANSACTIONS_CSV
88 + interim_dir: Path = INTERIM_DIR
89 + processed_dir: Path = PROCESSED_DIR
90 + first_week: str = FIRST_WEEK
91 + base_year: int = BASE_YEAR
92 + property_types: tuple[str, ...] = PROPERTY_TYPES
93 + model_version: str = field(default="0.1.0")
94 +
95 +
96 +def ensure_dirs() -> None:
97 + """Create all output/data directories that the pipeline writes into."""
98 + for d in (INTERIM_DIR, PROCESSED_DIR, EXTERNAL_DIR,
99 + FIGURES_DIR, TABLES_DIR, MAPS_DIR, REPORTS_DIR):
100 + d.mkdir(parents=True, exist_ok=True)
added engine/src/qwhpi/export.py +128 −0
@@ -0,0 +1,128 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/export.py
6 +# Purpose : Load each pipeline run into PostgreSQL (upsert, vintage-stamped).
7 +# =============================================================================
8 +"""Database loader.
9 +
10 +Upserts the canonical parquet outputs into the relational store defined by
11 +``db/schema.sql``. Connection comes from ``DATABASE_URL``
12 +(postgresql://...); when unset, the loader is a documented no-op — the API
13 +falls back to reading the Parquet lake directly (dev mode).
14 +"""
15 +
16 +from __future__ import annotations
17 +
18 +import hashlib
19 +import os
20 +
21 +import polars as pl
22 +
23 +from qwhpi.config import PROCESSED_DIR, RAW_TRANSACTIONS_CSV, REPO_ROOT
24 +
25 +
26 +def input_hash() -> str:
27 + h = hashlib.sha256()
28 + with open(RAW_TRANSACTIONS_CSV, "rb") as f:
29 + while chunk := f.read(1 << 22):
30 + h.update(chunk)
31 + return h.hexdigest()[:16]
32 +
33 +
34 +def load_database() -> bool:
35 + """Upsert the current canonical outputs into Postgres. Returns True if loaded."""
36 + url = os.environ.get("DATABASE_URL")
37 + if not url:
38 + print("DATABASE_URL not set — skipping DB load (API serves the Parquet lake).")
39 + return False
40 + import psycopg
41 +
42 + canon = pl.read_parquet(PROCESSED_DIR / "qwhpi_weekly.parquet")
43 + liq = pl.read_parquet(PROCESSED_DIR / "weekly_liquidity.parquet")
44 + fr = pl.read_parquet(PROCESSED_DIR / "first_release.parquet")
45 +
46 + with psycopg.connect(url) as conn, conn.cursor() as cur:
47 + cur.execute((REPO_ROOT / "db" / "schema.sql").read_text())
48 +
49 + geos = canon.select("geography_id", "geography_name", "geography_level").unique()
50 + parent = {"municipality": None, "region": "quebec", "province": None}
51 + for r in geos.sort(pl.col("geography_level").replace_strict(
52 + {"province": 0, "region": 1, "municipality": 2}, default=3)) \
53 + .iter_rows(named=True):
54 + cur.execute(
55 + """INSERT INTO geographies VALUES (%s,%s,%s,%s)
56 + ON CONFLICT (geography_id) DO UPDATE SET geography_name=EXCLUDED.geography_name""",
57 + (r["geography_id"], r["geography_name"], r["geography_level"],
58 + parent.get(r["geography_level"])),
59 + )
60 + series = canon.select("geography_id", "property_type").unique()
61 + for r in series.iter_rows(named=True):
62 + sid = f"{r['geography_id']}:{r['property_type']}"
63 + cur.execute(
64 + """INSERT INTO series (series_id, geography_id, property_type)
65 + VALUES (%s,%s,%s) ON CONFLICT DO NOTHING""",
66 + (sid, r["geography_id"], r["property_type"]),
67 + )
68 + cur.execute(
69 + """INSERT INTO model_runs (model_version, data_vintage, input_row_count, input_hash)
70 + VALUES (%s,%s,%s,%s) ON CONFLICT DO NOTHING""",
71 + (canon["model_version"][0], canon["data_vintage"][0],
72 + canon.height, input_hash()),
73 + )
74 +
75 + obs_cols = [c for c in canon.columns
76 + if c not in ("geography_name", "geography_level")]
77 + insert_obs = (
78 + "INSERT INTO observations (series_id, week, index, index_smoothed, "
79 + "representative_value, transactions, effective_sample_size, weekly_pct, "
80 + "four_week_pct, thirteen_week_pct, twenty_six_week_pct, yoy_pct, lower_95, "
81 + "upper_95, reliability_grade, shrinkage_weight, is_partial_week, "
82 + "model_version, data_vintage) VALUES (" + ",".join(["%s"] * 19) + ") "
83 + "ON CONFLICT (series_id, week) DO UPDATE SET "
84 + "index=EXCLUDED.index, index_smoothed=EXCLUDED.index_smoothed, "
85 + "representative_value=EXCLUDED.representative_value, "
86 + "transactions=EXCLUDED.transactions, "
87 + "effective_sample_size=EXCLUDED.effective_sample_size, "
88 + "weekly_pct=EXCLUDED.weekly_pct, four_week_pct=EXCLUDED.four_week_pct, "
89 + "thirteen_week_pct=EXCLUDED.thirteen_week_pct, "
90 + "twenty_six_week_pct=EXCLUDED.twenty_six_week_pct, yoy_pct=EXCLUDED.yoy_pct, "
91 + "lower_95=EXCLUDED.lower_95, upper_95=EXCLUDED.upper_95, "
92 + "reliability_grade=EXCLUDED.reliability_grade, "
93 + "shrinkage_weight=EXCLUDED.shrinkage_weight, "
94 + "is_partial_week=EXCLUDED.is_partial_week, "
95 + "model_version=EXCLUDED.model_version, data_vintage=EXCLUDED.data_vintage"
96 + )
97 + rows = [
98 + (f"{r['geography_id']}:{r['property_type']}", r["week"], r["index"],
99 + r["index_smoothed"], r["representative_value"], r["transactions"],
100 + r["effective_sample_size"], r["weekly_pct"], r["four_week_pct"],
101 + r["thirteen_week_pct"], r["twenty_six_week_pct"], r["yoy_pct"],
102 + r["lower_95"], r["upper_95"], r["reliability_grade"],
103 + r["shrinkage_weight"], r["is_partial_week"], r["model_version"],
104 + r["data_vintage"])
105 + for r in canon.iter_rows(named=True)
106 + ]
107 + cur.executemany(insert_obs, rows)
108 +
109 + cur.executemany(
110 + """INSERT INTO liquidity VALUES (%s,%s,%s)
111 + ON CONFLICT (series_id, week) DO UPDATE SET transactions=EXCLUDED.transactions""",
112 + [(f"{r['geography_id']}:{r['property_type']}", r["week"], r["transactions"])
113 + for r in liq.iter_rows(named=True)],
114 + )
115 + cur.executemany(
116 + """INSERT INTO vintages VALUES (%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING""",
117 + [(f"{r['geography_id']}:{r['property_type']}", r["week"],
118 + r["first_release_index"], r["first_release_index_smoothed"],
119 + r["first_release_vintage"])
120 + for r in fr.iter_rows(named=True)],
121 + )
122 + conn.commit()
123 + print(f"DB load complete: {canon.height:,} observations upserted.")
124 + return True
125 +
126 +
127 +if __name__ == "__main__":
128 + load_database()
added engine/src/qwhpi/features.py +103 −0
@@ -0,0 +1,103 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/features.py
6 +# Purpose : Hedonic feature engineering — age bins, log floor area with
7 +# explicit missingness handling, building type, location controls.
8 +# =============================================================================
9 +"""Feature engineering for hedonic estimation (Model A — structural only).
10 +
11 +Missingness is handled explicitly (CLAUDE.md §4.4): conditional-median
12 +imputation within (propertyType × municipality), falling back to
13 +(propertyType × region) then propertyType. No future-price information
14 +enters any imputation.
15 +
16 +TIME-CORRELATED MISSINGNESS RULE (v2, discovered 2026-08): a variable whose
17 +missingness moves over time cannot enter a time-dummy model — the "missing"
18 +category coefficient turns provider backfills into artificial index steps.
19 +Audit findings on this dataset:
20 +- ``buildingType`` was backfilled in region-staggered waves (most regions
21 + 2022-01, Montréal 2023-01, Outaouais complete from the start). It caused a
22 + −38 log-point artificial cliff in the Montréal single-family index and is
23 + therefore EXCLUDED from all hedonic models.
24 +- ``yearBuilt`` missingness drifts 15% → 2% over 2021–2023, so age gets
25 + conditional-median imputation instead of a "missing" bin (the imputation
26 + is time-invariant; a residual ``yb_missing`` flag is kept for diagnostics
27 + only, never as a regressor).
28 +- ``floorArea`` missingness is time-stable (1.6–2.4% every month), so its
29 + indicator ``fa_missing`` is harmless and kept in the model.
30 +"""
31 +
32 +from __future__ import annotations
33 +
34 +import polars as pl
35 +
36 +AGE_BIN_EDGES = [1, 5, 10, 20, 40, 60, 80, 120]
37 +AGE_BIN_LABELS = [
38 + "00_new_0-1", "01_2-5", "02_6-10", "03_11-20", "04_21-40",
39 + "05_41-60", "06_61-80", "07_81-120", "08_120plus",
40 +]
41 +
42 +
43 +def _age_bin_expr(col: str = "age_filled") -> pl.Expr:
44 + age = pl.col(col)
45 + expr = pl.when(age.is_null()).then(pl.lit(AGE_BIN_LABELS[4])) # defensive
46 + lo = None
47 + for edge, label in zip(AGE_BIN_EDGES, AGE_BIN_LABELS[:-1]):
48 + cond = age <= edge if lo is None else (age > lo) & (age <= edge)
49 + expr = expr.when(cond).then(pl.lit(label))
50 + lo = edge
51 + return expr.otherwise(pl.lit(AGE_BIN_LABELS[-1])).alias("age_bin")
52 +
53 +
54 +def build_features(df: pl.DataFrame) -> pl.DataFrame:
55 + """Attach hedonic features. Expects the clean table (with geography)."""
56 + out = df.with_columns(
57 + (pl.col("date").dt.year() - pl.col("yearBuilt")).clip(0, None).alias("age"),
58 + pl.col("week").dt.strftime("%Y-%m-%d").alias("week_str"),
59 + pl.col("date").dt.strftime("%Y-%m").alias("month"),
60 + # Forward-sortation area (first 3 chars of postal code) as the fine
61 + # location control; falls back to municipality when missing.
62 + pl.col("zipCode").str.slice(0, 3).str.to_uppercase().alias("fsa"),
63 + ).with_columns(
64 + # buildingType intentionally NOT a model feature (see module docstring)
65 + pl.col("buildingType").fill_null("missing").alias("building_type"),
66 + pl.when(pl.col("fsa").is_null())
67 + .then(pl.concat_str([pl.lit("MUN_"), pl.col("geo_code")]))
68 + .otherwise(pl.col("fsa"))
69 + .alias("loc_fine"),
70 + pl.col("floorArea").is_null().alias("fa_missing"),
71 + pl.col("yearBuilt").is_null().alias("yb_missing"), # diagnostics only
72 + )
73 +
74 + # Conditional-median imputation (no temporal information) for floorArea
75 + # and age — the imputation cells are time-invariant, so backfilled fields
76 + # cannot create index steps.
77 + def impute(col: str, filled: str) -> pl.DataFrame:
78 + nonlocal out
79 + med_mun = out.group_by(["propertyType", "geo_code"]).agg(
80 + pl.col(col).median().alias("_m_mun"))
81 + med_reg = out.group_by(["propertyType", "region_code"]).agg(
82 + pl.col(col).median().alias("_m_reg"))
83 + med_typ = out.group_by("propertyType").agg(
84 + pl.col(col).median().alias("_m_typ"))
85 + out = (
86 + out.join(med_mun, on=["propertyType", "geo_code"], how="left")
87 + .join(med_reg, on=["propertyType", "region_code"], how="left")
88 + .join(med_typ, on="propertyType", how="left")
89 + .with_columns(
90 + pl.coalesce(pl.col(col), pl.col("_m_mun"),
91 + pl.col("_m_reg"), pl.col("_m_typ")).alias(filled)
92 + )
93 + .drop("_m_mun", "_m_reg", "_m_typ")
94 + )
95 + return out
96 +
97 + impute("floorArea", "floor_area_filled")
98 + impute("age", "age_filled")
99 + out = out.with_columns(
100 + pl.col("floor_area_filled").log().alias("log_fa"),
101 + _age_bin_expr("age_filled"),
102 + )
103 + return out
added engine/src/qwhpi/geography.py +130 −0
@@ -0,0 +1,130 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/geography.py
6 +# Purpose : Spatial join of transaction lat/lng against official Quebec
7 +# administrative boundaries (SDA 1/20k) — municipality, MRC, region.
8 +# =============================================================================
9 +"""Geography layer.
10 +
11 +Authoritative boundary source (documented per CLAUDE.md §4.2):
12 +
13 +- Dataset : Découpages administratifs 1/20 000 (SDA), Gouvernement du Québec,
14 + Ministère des Ressources naturelles et des Forêts (MRNF).
15 +- URL : https://www.donneesquebec.ca/recherche/dataset/decoupages-administratifs
16 + (GPKG: https://diffusion.mern.gouv.qc.ca/diffusion/RGQ/Vectoriel/
17 + Theme/Local/SDA_20k/GPKG/SDA.gpkg.zip)
18 +- Version : V2026-07 (attribute ``MUS_CO_VER``)
19 +- CRS : EPSG:4269 (NAD83 geographic); metric ops use EPSG:32198
20 + (NAD83 / Quebec Lambert).
21 +
22 +The join is computed once and persisted to
23 +``data/processed/geo_join.parquet`` keyed by transaction ``id``. The raw
24 +``city`` text field is used only for validation, never as the geography.
25 +"""
26 +
27 +from __future__ import annotations
28 +
29 +import geopandas as gpd
30 +import numpy as np
31 +import pandas as pd
32 +import polars as pl
33 +
34 +from qwhpi.config import EXTERNAL_DIR, PROCESSED_DIR
35 +
36 +SDA_GPKG = EXTERNAL_DIR / "SDA.gpkg"
37 +SDA_VERSION = "V2026-07"
38 +CRS_GEO = "EPSG:4269" # NAD83 geographic — native CRS of the SDA layers
39 +CRS_METRIC = "EPSG:32198" # NAD83 / Quebec Lambert — metric fallback joins
40 +
41 +GEO_JOIN_PARQUET = PROCESSED_DIR / "geo_join.parquet"
42 +MUNICIPALITIES_PARQUET = PROCESSED_DIR / "municipalities.parquet"
43 +
44 +MUNIC_COLS = {
45 + "MUS_CO_GEO": "geo_code",
46 + "MUS_NM_MUN": "municipality",
47 + "MUS_CO_DES": "munic_type",
48 + "MUS_CO_MRC": "mrc_code",
49 + "MUS_NM_MRC": "mrc",
50 + "MUS_CO_REG": "region_code",
51 + "MUS_NM_REG": "region",
52 +}
53 +
54 +# Fallback tolerance for points that fall just outside every polygon
55 +# (coastline/river digitization slack). Beyond this the point is unassigned.
56 +NEAREST_MAX_METERS = 1000.0
57 +
58 +
59 +def load_municipal_layer() -> gpd.GeoDataFrame:
60 + """Load the SDA municipal polygons with harmonized column names."""
61 + gdf = gpd.read_file(SDA_GPKG, layer="munic_s", columns=list(MUNIC_COLS))
62 + gdf = gdf.rename(columns=MUNIC_COLS)
63 + # Drop Z dimension noise; geometry validity guard.
64 + gdf["geometry"] = gdf.geometry.force_2d()
65 + bad = ~gdf.geometry.is_valid
66 + if bad.any():
67 + gdf.loc[bad, "geometry"] = gdf.loc[bad, "geometry"].buffer(0)
68 + return gdf
69 +
70 +
71 +def spatial_join(tx: pl.DataFrame) -> pl.DataFrame:
72 + """Join each transaction point to a municipality (and thus MRC + region).
73 +
74 + Returns one row per transaction id with geography attributes and a
75 + ``join_method`` in {"within", "nearest<=1km", "unassigned"}.
76 + """
77 + munic = load_municipal_layer()
78 +
79 + pts_df = tx.select("id", "lat", "lng").to_pandas()
80 + pts = gpd.GeoDataFrame(
81 + pts_df[["id"]],
82 + geometry=gpd.points_from_xy(pts_df["lng"], pts_df["lat"], crs="EPSG:4326"),
83 + ).to_crs(CRS_GEO)
84 +
85 + joined = gpd.sjoin(pts, munic, how="left", predicate="within")
86 + # A point on a shared border can match twice — keep the first match.
87 + joined = joined[~joined.index.duplicated(keep="first")]
88 + joined["join_method"] = np.where(joined["geo_code"].notna(), "within", None)
89 +
90 + # Nearest-polygon fallback for the residual, capped at NEAREST_MAX_METERS.
91 + missing_mask = joined["geo_code"].isna()
92 + if missing_mask.any():
93 + rest = joined.loc[missing_mask, ["id", "geometry"]].to_crs(CRS_METRIC)
94 + near = gpd.sjoin_nearest(
95 + rest, munic.to_crs(CRS_METRIC), how="left",
96 + max_distance=NEAREST_MAX_METERS, distance_col="join_dist_m",
97 + )
98 + near = near[~near.index.duplicated(keep="first")]
99 + for col in MUNIC_COLS.values():
100 + joined.loc[missing_mask, col] = near[col]
101 + joined.loc[missing_mask, "join_method"] = np.where(
102 + near["geo_code"].notna(), "nearest<=1km", "unassigned"
103 + )
104 +
105 + out = pd.DataFrame(joined.drop(columns=["geometry"]))
106 + keep = ["id", *MUNIC_COLS.values(), "join_method"]
107 + return pl.from_pandas(out[keep])
108 +
109 +
110 +def build_geo_join(tx: pl.DataFrame, force: bool = False) -> pl.DataFrame:
111 + """Compute (or load the cached) transaction -> geography join."""
112 + if GEO_JOIN_PARQUET.exists() and not force:
113 + cached = pl.read_parquet(GEO_JOIN_PARQUET)
114 + if cached.height == tx.height:
115 + return cached
116 + result = spatial_join(tx)
117 + PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
118 + result.write_parquet(GEO_JOIN_PARQUET)
119 +
120 + # Slim municipality lookup for the API / dashboard.
121 + lookup = (
122 + result.filter(pl.col("geo_code").is_not_null())
123 + .group_by(["geo_code", "municipality", "munic_type", "mrc_code", "mrc",
124 + "region_code", "region"])
125 + .len()
126 + .rename({"len": "n_transactions"})
127 + .sort("n_transactions", descending=True)
128 + )
129 + lookup.write_parquet(MUNICIPALITIES_PARQUET)
130 + return result
added engine/src/qwhpi/hedonic.py +104 −0
@@ -0,0 +1,104 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/hedonic.py
6 +# Purpose : Pooled hedonic time-dummy estimation (Model A) with absorbed
7 +# location fixed effects; week-coefficient extraction and rebasing.
8 +# =============================================================================
9 +"""Pooled hedonic time-dummy estimation.
10 +
11 +Model A (headline, structural characteristics only):
12 +
13 + log(amount) = Σ_t δ_t · 1[week = t] + β·log(floorArea) + fa_missing
14 + + age bins + building type (+ property type when pooled)
15 + + location FE (absorbed) + ε
16 +
17 +Estimated ONCE over the pooled 2021→present sample per published cell —
18 +never as independent weekly regressions. Week coefficients δ_t form the
19 +latent log index; the published index is exp(δ_t) rebased so the 2021
20 +average equals 100. SEs are heteroskedasticity-robust (HC1); block-bootstrap
21 +CIs come from uncertainty.py at the hierarchical stage.
22 +"""
23 +
24 +from __future__ import annotations
25 +
26 +from dataclasses import dataclass
27 +
28 +import numpy as np
29 +import pandas as pd
30 +import polars as pl
31 +import pyfixest as pf
32 +
33 +from qwhpi.config import BASE_YEAR
34 +
35 +STRUCTURAL_RHS = "log_fa + fa_missing + C(age_bin)" # buildingType excluded: time-correlated missingness (features.py)
36 +
37 +
38 +@dataclass
39 +class TimeDummyResult:
40 + """Weekly index extracted from a pooled time-dummy regression."""
41 +
42 + table: pl.DataFrame # week, log_index, se, index, lower_95, upper_95
43 + n_obs: int
44 + r2: float
45 +
46 +
47 +def _rebase(weeks: np.ndarray, delta: np.ndarray, se: np.ndarray) -> pl.DataFrame:
48 + """Rebase week log-effects so the BASE_YEAR average is 100."""
49 + years = np.array([w[:4] for w in weeks])
50 + base_mask = years == str(BASE_YEAR)
51 + base_mean = delta[base_mask].mean()
52 + log_index = delta - base_mean
53 + index = np.exp(log_index) * 100.0
54 + return pl.DataFrame({
55 + "week": weeks,
56 + "log_index": log_index,
57 + "se": se,
58 + "index": index,
59 + "lower_95": np.exp(log_index - 1.96 * se) * 100.0,
60 + "upper_95": np.exp(log_index + 1.96 * se) * 100.0,
61 + }).sort("week")
62 +
63 +
64 +def estimate_time_dummy(
65 + df: pl.DataFrame,
66 + absorb: str = "loc_fine",
67 + include_property_type: bool = False,
68 + min_obs: int = 500,
69 +) -> TimeDummyResult | None:
70 + """Estimate the pooled time-dummy model on one publication cell.
71 +
72 + ``df`` must carry features from :func:`qwhpi.features.build_features`.
73 + Returns None when the cell is too thin to estimate at all (handled by the
74 + hierarchical model instead).
75 + """
76 + if df.height < min_obs:
77 + return None
78 +
79 + rhs = STRUCTURAL_RHS + (" + C(propertyType)" if include_property_type else "")
80 + cols = ["log_amount", "week_str", "log_fa", "fa_missing", "age_bin",
81 + "building_type", "propertyType", absorb]
82 + pdf = df.select(cols).to_pandas()
83 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
84 +
85 + # Reference week = first week present in the cell.
86 + ref = sorted(pdf["week_str"].unique())[0]
87 + fml = f"log_amount ~ i(week_str, ref='{ref}') + {rhs} | {absorb}"
88 + fit = pf.feols(fml, data=pdf, vcov="HC1")
89 +
90 + coefs = fit.coef()
91 + ses = fit.se()
92 + weeks, deltas, sess = [ref], [0.0], [0.0]
93 + prefix = "week_str::"
94 + for name, val in coefs.items():
95 + if name.startswith(prefix):
96 + weeks.append(name.removeprefix(prefix))
97 + deltas.append(float(val))
98 + sess.append(float(ses[name]))
99 +
100 + order = np.argsort(weeks)
101 + table = _rebase(np.array(weeks)[order],
102 + np.array(deltas)[order],
103 + np.array(sess)[order])
104 + return TimeDummyResult(table=table, n_obs=fit._N, r2=float(fit._r2))
added engine/src/qwhpi/hierarchy.py +228 −0
@@ -0,0 +1,228 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/hierarchy.py
6 +# Purpose : Hierarchical weekly index — stage-1 pooled residualization, then
7 +# Kalman-shrunk deviations (province → region → municipality).
8 +# =============================================================================
9 +"""Hierarchical weekly state model (two-stage architecture).
10 +
11 +Stage 1 (pooled hedonic residualization)
12 + log(P) = structural chars + loc FE + (week × type) FE + ε
13 + - the (week × type) fixed effects ARE the province-level weekly paths
14 + per property type (liquid: >=300 tx/week each);
15 + - residuals ε are quality- and location-adjusted deviations of each
16 + transaction from its type's province path.
17 +
18 +Stage 2 (hierarchical shrinkage)
19 + For each cell below the province the weekly mean residual is smoothed by
20 + a heteroskedastic local-level Kalman model (state_space.py):
21 + region cell : deviation of region×type from the province path
22 + municipality : deviation of municipality×type from the smoothed
23 + region path (residual − region deviation)
24 + Thin weeks carry high observation noise -> the filter shrinks toward the
25 + parent path (gain ~ 0). Liquid weeks dominate their own estimate.
26 +
27 +Cell log index = province type path + region deviation (+ municipality
28 +deviation), rebased to 2021 avg = 100. The "all" type per geography is a
29 +fixed-weight (full-sample transaction share) combination of the three type
30 +log-indexes, keeping composition constant.
31 +
32 +Published variants (§4.8):
33 + index — raw weekly: path + *unsmoothed* weekly mean deviation
34 + index_smoothed — one-sided filtered (real-time, no lookahead)
35 +The two-sided smoothed path is kept for research as ``index_research``.
36 +"""
37 +
38 +from __future__ import annotations
39 +
40 +from dataclasses import dataclass
41 +
42 +import numpy as np
43 +import pandas as pd
44 +import polars as pl
45 +import pyfixest as pf
46 +
47 +from qwhpi.config import BASE_YEAR
48 +from qwhpi.state_space import LocalLevelFit, fit_local_level
49 +
50 +STRUCTURAL_RHS = "log_fa + fa_missing + C(age_bin)" # buildingType excluded: time-correlated missingness (features.py)
51 +MIN_SIGMA2_OBS = 50 # min cell size to estimate its own residual variance
52 +
53 +
54 +# --------------------------------------------------------------------------- #
55 +# Stage 1
56 +# --------------------------------------------------------------------------- #
57 +
58 +@dataclass
59 +class Stage1Result:
60 + """Province paths + aligned residuals from the pooled regression."""
61 +
62 + paths: pl.DataFrame # week, property_type, delta (log path, unnormalized)
63 + residuals: pl.DataFrame # id, week_str, propertyType, region_code,
64 + # geo_code, municipality, resid
65 + n_obs: int
66 + r2: float
67 +
68 +
69 +def stage1_residualize(feat: pl.DataFrame) -> Stage1Result:
70 + """Pooled regression over the full research sample (all three types)."""
71 + cols = ["id", "log_amount", "week_str", "propertyType", "log_fa",
72 + "fa_missing", "age_bin", "building_type", "loc_fine",
73 + "region_code", "geo_code", "municipality"]
74 + pdf = feat.select(cols).to_pandas()
75 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
76 + pdf["wk_type"] = pdf["week_str"] + "|" + pdf["propertyType"]
77 +
78 + fit = pf.feols(
79 + f"log_amount ~ {STRUCTURAL_RHS} | loc_fine + wk_type",
80 + data=pdf, vcov="HC1",
81 + )
82 + fx = fit.fixef()
83 + wk_key = next(k for k in fx if "wk_type" in k)
84 + wk_fx = fx[wk_key]
85 + paths = pl.DataFrame({
86 + "week": [k.split("|")[0] for k in wk_fx],
87 + "property_type": [k.split("|")[1] for k in wk_fx],
88 + "delta": [float(v) for v in wk_fx.values()],
89 + }).sort(["property_type", "week"])
90 +
91 + yhat = fit.predict(newdata=pdf)
92 + pdf["resid"] = pdf["log_amount"].values - np.asarray(yhat, dtype=float)
93 + resid = pl.from_pandas(
94 + pdf[["id", "week_str", "propertyType", "region_code", "geo_code",
95 + "municipality", "resid"]]
96 + ).filter(pl.col("resid").is_not_nan() & pl.col("resid").is_not_null())
97 + return Stage1Result(paths=paths, residuals=resid,
98 + n_obs=fit._N, r2=float(fit._r2))
99 +
100 +
101 +# --------------------------------------------------------------------------- #
102 +# Stage 2 helpers
103 +# --------------------------------------------------------------------------- #
104 +
105 +def week_grid(residuals: pl.DataFrame) -> list[str]:
106 + """Continuous Monday grid over the sample, as ISO strings."""
107 + import datetime as dt
108 + weeks = sorted(residuals["week_str"].unique().to_list())
109 + first = dt.date.fromisoformat(weeks[0])
110 + last = dt.date.fromisoformat(weeks[-1])
111 + grid = []
112 + cur = first
113 + while cur <= last:
114 + grid.append(cur.isoformat())
115 + cur += dt.timedelta(days=7)
116 + return grid
117 +
118 +
119 +@dataclass
120 +class CellDeviation:
121 + """Smoothed weekly deviation of one cell from its parent path."""
122 +
123 + weeks: list[str]
124 + raw: np.ndarray # unsmoothed weekly mean deviation (NaN if n=0)
125 + n: np.ndarray
126 + fit: LocalLevelFit
127 +
128 +
129 +def cell_deviation(
130 + dev_frame: pl.DataFrame,
131 + grid: list[str],
132 + fallback_sigma2: float,
133 +) -> CellDeviation:
134 + """Weekly mean deviation + Kalman fit for one cell.
135 +
136 + ``dev_frame`` must have columns ``week_str`` and ``dev`` (per-transaction
137 + deviation from the parent path).
138 + """
139 + agg = (
140 + dev_frame.group_by("week_str")
141 + .agg(pl.col("dev").mean().alias("y"),
142 + pl.len().alias("n"),
143 + pl.col("dev").var().alias("v"))
144 + )
145 + lookup = {r["week_str"]: r for r in agg.iter_rows(named=True)}
146 + y = np.array([lookup[w]["y"] if w in lookup else np.nan for w in grid])
147 + n = np.array([lookup[w]["n"] if w in lookup else 0 for w in grid], dtype=float)
148 +
149 + if dev_frame.height >= MIN_SIGMA2_OBS:
150 + sigma2 = float(dev_frame["dev"].var())
151 + else:
152 + sigma2 = fallback_sigma2
153 + fit = fit_local_level(y, n, sigma2)
154 + return CellDeviation(weeks=grid, raw=y, n=n, fit=fit)
155 +
156 +
157 +# --------------------------------------------------------------------------- #
158 +# Index composition
159 +# --------------------------------------------------------------------------- #
160 +
161 +def rebase_log(weeks: list[str], log_path: np.ndarray) -> np.ndarray:
162 + """Shift a log path so the BASE_YEAR average is 0 (index 100)."""
163 + return log_path - base_constant(weeks, log_path)
164 +
165 +
166 +def base_constant(weeks: list[str], log_path: np.ndarray) -> float:
167 + """Mean of the log path over BASE_YEAR (the rebasing constant)."""
168 + mask = np.array([w.startswith(str(BASE_YEAR)) for w in weeks])
169 + valid = mask & np.isfinite(log_path)
170 + return float(log_path[valid].mean())
171 +
172 +
173 +def compose_cell_table(
174 + *,
175 + grid: list[str],
176 + province_path: np.ndarray, # aligned to grid (log, unnormalized)
177 + deviations: list[CellDeviation], # region [+ municipality]
178 + geography_id: str,
179 + geography_name: str,
180 + geography_level: str,
181 + property_type: str,
182 + path_var: np.ndarray, # variance of the province path per week
183 +) -> pl.DataFrame:
184 + """Assemble the published series for one cell."""
185 + dev_filtered = sum(d.fit.filtered for d in deviations)
186 + dev_filtered_var = sum(d.fit.filtered_var for d in deviations)
187 + dev_smoothed = sum(d.fit.smoothed for d in deviations)
188 + dev_smoothed_var = sum(d.fit.smoothed_var for d in deviations)
189 + # Raw deviation: last level's raw mean plus parents' smoothed paths.
190 + if deviations:
191 + raw_last = deviations[-1].raw
192 + parents = sum(d.fit.smoothed for d in deviations[:-1]) if len(deviations) > 1 else 0.0
193 + dev_raw = parents + raw_last
194 + n = deviations[-1].n
195 + gain = deviations[-1].fit.gain
196 + else:
197 + dev_raw = np.zeros(len(grid))
198 + n = np.full(len(grid), np.nan)
199 + gain = np.ones(len(grid))
200 +
201 + # One shared normalization constant (from the always-finite filtered
202 + # path) so raw / filtered / smoothed variants are directly comparable
203 + # even when a thin cell has no raw observation in the base year.
204 + base = base_constant(grid, province_path + dev_filtered)
205 + log_raw = province_path + dev_raw - base
206 + log_filt = province_path + dev_filtered - base
207 + log_smooth = province_path + dev_smoothed - base
208 +
209 + var_filt = path_var + dev_filtered_var
210 + se = np.sqrt(np.maximum(var_filt, 0.0))
211 +
212 + return pl.DataFrame({
213 + "week": grid,
214 + "geography_id": geography_id,
215 + "geography_name": geography_name,
216 + "geography_level": geography_level,
217 + "property_type": property_type,
218 + "index": np.exp(log_raw) * 100.0,
219 + "index_smoothed": np.exp(log_filt) * 100.0,
220 + "index_research": np.exp(log_smooth) * 100.0,
221 + "log_index": log_filt,
222 + "se_log": se,
223 + "lower_95": np.exp(log_filt - 1.96 * se) * 100.0,
224 + "upper_95": np.exp(log_filt + 1.96 * se) * 100.0,
225 + "transactions": n,
226 + "shrinkage_weight": 1.0 - gain, # share coming from the parent path
227 + "research_var": path_var + dev_smoothed_var,
228 + })
added engine/src/qwhpi/index.py +139 −0
@@ -0,0 +1,139 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/index.py
6 +# Purpose : Canonical dataset assembly — growth horizons, reliability grades,
7 +# representative values, effective sample size, partial-week flag.
8 +# =============================================================================
9 +"""Assembly of the canonical weekly table (CLAUDE.md §4.8–4.9).
10 +
11 +Definitions
12 +-----------
13 +representative_value
14 + Fixed-basket dollar value: B_cell · index_smoothed/100 where
15 + B_cell = exp( mean_i[ log P_i − log(I_smoothed(week_i)/100) ] ) over the
16 + cell's full research sample — i.e. the cell's quality-mix-constant
17 + geometric mean price expressed at base-period (2021 avg) prices, carried
18 + forward by the index. Documented in the methodology paper.
19 +
20 +effective_sample_size
21 + Information content of the filtered estimate in transaction units:
22 + σ²_cell / Var(filtered deviation). For province cells (no deviation
23 + filter) it equals the weekly transaction count.
24 +
25 +reliability_grade (A–E)
26 + Empirically anchored on the downsampling experiment
27 + (outputs/tables/downsampling_results.csv): CI coverage and RMSE stay
28 + strong down to ~25 tx/week and degrade below ~15.
29 + A: median cell liquidity ≥ 50 tx/wk and se_log < 1.5%
30 + B: ≥ 20 tx/wk and se_log < 2.5%
31 + C: ≥ 10 tx/wk (documented shrinkage; CI approximately calibrated)
32 + D: ≥ 5 tx/wk (heavy shrinkage — parent-dominated weeks)
33 + E: < 5 tx/wk (model-implied; published for transparency only)
34 + The grade is a per-cell base grade (median liquidity) downgraded one
35 + notch in weeks where the local week is empty (transactions == 0).
36 +
37 +is_partial_week
38 + Weeks (at the very end of the sample) whose province-wide volume is
39 + below 60% of the trailing 8-week median — registration lag means the
40 + latest week is a nowcast and will revise.
41 +"""
42 +
43 +from __future__ import annotations
44 +
45 +import numpy as np
46 +import polars as pl
47 +
48 +GROWTH_HORIZONS = {
49 + "weekly_pct": 1,
50 + "four_week_pct": 4,
51 + "thirteen_week_pct": 13,
52 + "twenty_six_week_pct": 26,
53 + "yoy_pct": 52,
54 +}
55 +
56 +
57 +def add_growth(df: pl.DataFrame, col: str = "index_smoothed") -> pl.DataFrame:
58 + """Attach growth-rate columns per series (sorted within cell)."""
59 + keys = ["geography_id", "property_type"]
60 + df = df.sort([*keys, "week"])
61 + exprs = [
62 + ((pl.col(col) / pl.col(col).shift(h).over(keys) - 1) * 100)
63 + .round(3).alias(name)
64 + for name, h in GROWTH_HORIZONS.items()
65 + ]
66 + return df.with_columns(exprs)
67 +
68 +
69 +def grade_expr() -> pl.Expr:
70 + """Reliability grade from cell median liquidity, SE, and week emptiness."""
71 + med = pl.col("cell_median_tx")
72 + se = pl.col("se_log")
73 + base = (
74 + pl.when((med >= 50) & (se < 0.015)).then(pl.lit("A"))
75 + .when((med >= 20) & (se < 0.025)).then(pl.lit("B"))
76 + .when(med >= 10).then(pl.lit("C"))
77 + .when(med >= 5).then(pl.lit("D"))
78 + .otherwise(pl.lit("E"))
79 + )
80 + downgrade = {"A": "B", "B": "C", "C": "D", "D": "E", "E": "E"}
81 + return (
82 + pl.when(pl.col("transactions").fill_nan(0) == 0)
83 + .then(base.replace(downgrade))
84 + .otherwise(base)
85 + .alias("reliability_grade")
86 + )
87 +
88 +
89 +def add_reliability(df: pl.DataFrame) -> pl.DataFrame:
90 + keys = ["geography_id", "property_type"]
91 + med = df.group_by(keys).agg(
92 + pl.col("transactions").fill_nan(None).median().alias("cell_median_tx"))
93 + return df.join(med, on=keys, how="left").with_columns(grade_expr())
94 +
95 +
96 +def add_partial_week(df: pl.DataFrame, province_volume: pl.DataFrame) -> pl.DataFrame:
97 + """Flag trailing weeks with abnormally low province-wide volume."""
98 + pv = province_volume.sort("week")
99 + vols = pv["volume"].to_numpy().astype(float)
100 + med8 = np.array([np.median(vols[max(0, i - 8):i]) if i else vols[0]
101 + for i in range(len(vols))])
102 + partial = vols < 0.6 * med8
103 + # Only flag a trailing contiguous run (mid-sample dips are real market lulls).
104 + flagged = np.zeros(len(vols), dtype=bool)
105 + i = len(vols) - 1
106 + while i >= 0 and partial[i]:
107 + flagged[i] = True
108 + i -= 1
109 + pw = pl.DataFrame({"week": pv["week"], "is_partial_week": flagged})
110 + return df.join(pw, on="week", how="left").with_columns(
111 + pl.col("is_partial_week").fill_null(False))
112 +
113 +
114 +def add_representative_value(df: pl.DataFrame, basket: pl.DataFrame) -> pl.DataFrame:
115 + """basket: geography_id, property_type, basket_value (2021-price dollars)."""
116 + return df.join(basket, on=["geography_id", "property_type"], how="left") \
117 + .with_columns(
118 + (pl.col("basket_value") * pl.col("index_smoothed") / 100.0)
119 + .round(0).alias("representative_value")
120 + )
121 +
122 +
123 +def compute_basket(clean_cells: pl.DataFrame, index_df: pl.DataFrame) -> pl.DataFrame:
124 + """Fixed-basket base-period value per cell (see module docstring).
125 +
126 + ``clean_cells`` must have geography_id, property_type, week, log_amount.
127 + """
128 + j = clean_cells.join(
129 + index_df.select("geography_id", "property_type", "week",
130 + pl.col("index_smoothed")),
131 + on=["geography_id", "property_type", "week"], how="inner",
132 + )
133 + return (
134 + j.group_by(["geography_id", "property_type"])
135 + .agg(
136 + (pl.col("log_amount") - (pl.col("index_smoothed") / 100.0).log())
137 + .mean().exp().round(0).alias("basket_value")
138 + )
139 + )
added engine/src/qwhpi/ingest.py +81 −0
@@ -0,0 +1,81 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/ingest.py
6 +# Purpose : Load the immutable raw transactions CSV with typed columns and
7 +# attach the Monday-labeled weekly calendar.
8 +# =============================================================================
9 +"""Raw-data ingestion.
10 +
11 +The raw CSV in ``data/raw/`` is immutable: this module only reads it. Typed
12 +loading + week assignment happen here so every downstream stage sees the same
13 +representation. The parsed frame is cached to ``data/interim/transactions_raw.parquet``
14 +keyed on the raw file's size/mtime, so repeat runs skip CSV parsing.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import json
20 +from pathlib import Path
21 +
22 +import polars as pl
23 +
24 +from qwhpi.config import INTERIM_DIR, RAW_TRANSACTIONS_CSV
25 +
26 +RAW_SCHEMA: dict[str, pl.DataType] = {
27 + "id": pl.Utf8,
28 + "date": pl.Utf8, # parsed to Date below
29 + "amount": pl.Float64,
30 + "street": pl.Utf8,
31 + "zipCode": pl.Utf8,
32 + "city": pl.Utf8,
33 + "lat": pl.Float64,
34 + "lng": pl.Float64,
35 + "propertyType": pl.Utf8,
36 + "yearBuilt": pl.Int64,
37 + "floorArea": pl.Float64,
38 + "buildingType": pl.Utf8,
39 + "previousValue": pl.Float64,
40 + "totalArValue": pl.Float64,
41 + "ownerType": pl.Utf8,
42 +}
43 +
44 +CACHE_PARQUET = INTERIM_DIR / "transactions_raw.parquet"
45 +CACHE_MANIFEST = INTERIM_DIR / "transactions_raw.manifest.json"
46 +
47 +
48 +def _raw_fingerprint(csv_path: Path) -> dict:
49 + stat = csv_path.stat()
50 + return {"path": str(csv_path), "size": stat.st_size, "mtime": stat.st_mtime}
51 +
52 +
53 +def load_raw(csv_path: Path = RAW_TRANSACTIONS_CSV, use_cache: bool = True) -> pl.DataFrame:
54 + """Load raw transactions with types, date parsing, and week assignment.
55 +
56 + Adds columns:
57 + - ``week``: Monday of the transaction's Monday->Sunday week (pl.Date)
58 + - ``log_amount``: natural log of amount (null when amount <= 0)
59 + """
60 + if use_cache and CACHE_PARQUET.exists() and CACHE_MANIFEST.exists():
61 + manifest = json.loads(CACHE_MANIFEST.read_text())
62 + if manifest == _raw_fingerprint(csv_path):
63 + return pl.read_parquet(CACHE_PARQUET)
64 +
65 + df = pl.read_csv(csv_path, schema_overrides=RAW_SCHEMA)
66 + df = df.with_columns(
67 + pl.col("date").str.to_date("%Y-%m-%d"),
68 + ).with_columns(
69 + # Monday-labeled week: subtract the ISO weekday offset (Mon=1 ... Sun=7).
70 + (pl.col("date") - pl.duration(days=pl.col("date").dt.weekday() - 1)).alias("week"),
71 + pl.when(pl.col("amount") > 0)
72 + .then(pl.col("amount").log())
73 + .otherwise(None)
74 + .alias("log_amount"),
75 + )
76 +
77 + if use_cache:
78 + INTERIM_DIR.mkdir(parents=True, exist_ok=True)
79 + df.write_parquet(CACHE_PARQUET)
80 + CACHE_MANIFEST.write_text(json.dumps(_raw_fingerprint(csv_path)))
81 + return df
added engine/src/qwhpi/nowcast.py +40 −0
@@ -0,0 +1,40 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/nowcast.py
6 +# Purpose : Partial-week (nowcast) detection for the latest weeks — data
7 +# registration lag means trailing weeks under-report volume.
8 +# =============================================================================
9 +"""Nowcast handling for trailing partial weeks.
10 +
11 +Land-registry transactions arrive with a registration lag: the most recent
12 +Monday-week typically shows a fraction of its eventual volume. Such weeks
13 +are still published (the filtered state-space estimate is a genuine
14 +nowcast) but flagged ``is_partial_week = true`` so consumers know the value
15 +will revise. Detection: a trailing contiguous run of weeks whose
16 +province-wide volume is below ``PARTIAL_THRESHOLD`` × trailing-8-week
17 +median volume.
18 +"""
19 +
20 +from __future__ import annotations
21 +
22 +import numpy as np
23 +import polars as pl
24 +
25 +PARTIAL_THRESHOLD = 0.6
26 +
27 +
28 +def partial_week_flags(province_volume: pl.DataFrame) -> pl.DataFrame:
29 + """province_volume: columns week, volume — returns week, is_partial_week."""
30 + pv = province_volume.sort("week")
31 + vols = pv["volume"].to_numpy().astype(float)
32 + med8 = np.array([np.median(vols[max(0, i - 8):i]) if i else vols[0]
33 + for i in range(len(vols))])
34 + low = vols < PARTIAL_THRESHOLD * med8
35 + flagged = np.zeros(len(vols), dtype=bool)
36 + i = len(vols) - 1
37 + while i >= 0 and low[i]:
38 + flagged[i] = True
39 + i -= 1
40 + return pl.DataFrame({"week": pv["week"], "is_partial_week": flagged})
added engine/src/qwhpi/plotting.py +63 −0
@@ -0,0 +1,63 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/plotting.py
6 +# Purpose : Shared plotting configuration — one validated palette and mark
7 +# spec for every figure (matches the dashboard).
8 +# =============================================================================
9 +"""Shared figure style.
10 +
11 +Categorical palette (validated, fixed order — never cycled beyond need):
12 +blue, orange, aqua, yellow, magenta. Sequential ramp: blue light→dark.
13 +Marks: 2px lines, recessive grid, no chartjunk. Author credit on every
14 +figure footer.
15 +"""
16 +
17 +from __future__ import annotations
18 +
19 +import matplotlib
20 +
21 +matplotlib.use("Agg")
22 +import matplotlib.pyplot as plt # noqa: E402
23 +
24 +SERIES = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4"]
25 +BAND = (42 / 255, 120 / 255, 214 / 255, 0.14)
26 +SEQ = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95", "#0d366b"]
27 +TEXT = "#0b0b0b"
28 +TEXT2 = "#52514e"
29 +GRID = "#dddcd6"
30 +
31 +plt.rcParams.update({
32 + "figure.facecolor": "#fcfcfb",
33 + "axes.facecolor": "#fcfcfb",
34 + "axes.edgecolor": GRID,
35 + "axes.grid": True,
36 + "grid.color": GRID,
37 + "grid.linestyle": ":",
38 + "grid.linewidth": 0.8,
39 + "axes.spines.top": False,
40 + "axes.spines.right": False,
41 + "text.color": TEXT,
42 + "axes.labelcolor": TEXT2,
43 + "xtick.color": TEXT2,
44 + "ytick.color": TEXT2,
45 + "font.size": 10,
46 + "axes.titlesize": 11,
47 + "figure.dpi": 150,
48 + "lines.linewidth": 2.0,
49 + "legend.frameon": False,
50 +})
51 +
52 +
53 +def credit(fig: "plt.Figure") -> None:
54 + fig.text(0.99, 0.01, "QWHPI — Simon-Pierre Boucher · contact@spboucher.ai",
55 + ha="right", va="bottom", fontsize=7, color=TEXT2)
56 +
57 +
58 +def save(fig: "plt.Figure", path) -> None:
59 + credit(fig)
60 + fig.tight_layout(rect=(0, 0.02, 1, 1))
61 + fig.savefig(path, bbox_inches="tight")
62 + plt.close(fig)
63 + print(f" -> {path}")
added engine/src/qwhpi/repeat_sales.py +75 −0
@@ -0,0 +1,75 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/repeat_sales.py
6 +# Purpose : Repeat-sales (BMN) index as a directional validation check for
7 +# the hedonic indexes. Condo/plex pairs are unit-signature matched.
8 +# =============================================================================
9 +"""Repeat-sales validation index (Bailey–Muth–Nourse).
10 +
11 +Address caveat (CLAUDE.md §4.7): condo and plex street strings carry no unit
12 +numbers, so a bare (street, city) match pairs *different units in the same
13 +building*. Pairs are therefore matched on the **unit signature**
14 +(street, city, floorArea, yearBuilt): either the same unit or an identical-
15 +spec unit in the same building — both valid for price-relative measurement.
16 +Sales less than 90 days apart are dropped (flips / double registrations).
17 +
18 +Estimation: log(P₂/P₁) = Σ_t δ_t (D_{t,2} − D_{t,1}) + ε at monthly
19 +frequency (weekly repeat pairs are too sparse), OLS via lstsq. The index is
20 +rebased to the 2021 average = 100 for comparability with QWHPI.
21 +"""
22 +
23 +from __future__ import annotations
24 +
25 +import numpy as np
26 +import polars as pl
27 +
28 +MIN_DAYS_BETWEEN_SALES = 90
29 +PAIR_KEY = ["street", "city", "floorArea", "yearBuilt"]
30 +
31 +
32 +def build_pairs(df: pl.DataFrame) -> pl.DataFrame:
33 + """Consecutive same-signature sale pairs with log price relatives."""
34 + d = df.filter(
35 + pl.col("street").is_not_null()
36 + & pl.col("floorArea").is_not_null()
37 + & pl.col("yearBuilt").is_not_null()
38 + ).sort("date")
39 + d = d.with_columns(
40 + pl.col("log_amount").shift(1).over(PAIR_KEY).alias("lp_prev"),
41 + pl.col("date").shift(1).over(PAIR_KEY).alias("dt_prev"),
42 + ).filter(pl.col("lp_prev").is_not_null())
43 + d = d.filter(
44 + (pl.col("date") - pl.col("dt_prev")).dt.total_days() >= MIN_DAYS_BETWEEN_SALES
45 + )
46 + return d.with_columns(
47 + (pl.col("log_amount") - pl.col("lp_prev")).alias("dlp"),
48 + pl.col("date").dt.strftime("%Y-%m").alias("m2"),
49 + pl.col("dt_prev").dt.strftime("%Y-%m").alias("m1"),
50 + )
51 +
52 +
53 +def bmn_index(pairs: pl.DataFrame) -> pl.DataFrame | None:
54 + """Monthly BMN index (2021 avg = 100). None when pairs are too few."""
55 + if pairs.height < 200:
56 + return None
57 + months = sorted(set(pairs["m1"].to_list()) | set(pairs["m2"].to_list()))
58 + pos = {m: i for i, m in enumerate(months)}
59 + n, T = pairs.height, len(months)
60 + X = np.zeros((n, T - 1)) # first month is the base
61 + m1 = pairs["m1"].to_numpy()
62 + m2 = pairs["m2"].to_numpy()
63 + for i in range(n):
64 + if pos[m2[i]] > 0:
65 + X[i, pos[m2[i]] - 1] += 1.0
66 + if pos[m1[i]] > 0:
67 + X[i, pos[m1[i]] - 1] -= 1.0
68 + y = pairs["dlp"].to_numpy()
69 + beta, *_ = np.linalg.lstsq(X, y, rcond=None)
70 + delta = np.concatenate([[0.0], beta])
71 + base = delta[[m.startswith("2021") for m in months]].mean()
72 + return pl.DataFrame({
73 + "month": months,
74 + "rs_index": np.exp(delta - base) * 100.0,
75 + })
added engine/src/qwhpi/rtd.py +289 −0
@@ -0,0 +1,289 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/rtd.py
6 +# Purpose : Rolling-Time-Dummy stage 1 (v2, monthly) — 13-month Huber-robust
7 +# hedonic windows, mean-splice linking, real-time residuals.
8 +# =============================================================================
9 +"""Rolling-Time-Dummy (RTD) hedonic estimation — monthly, robust.
10 +
11 +Method (see outputs/reports/methodology_v2_research.md for sources):
12 +
13 +- 13-month windows stepped one month at a time. Within each window the
14 + hedonic is estimated by IRLS with Huber weights (k = 1.345 on
15 + MAD-standardized residuals, ``N_IRLS`` reweighting passes), absorbing
16 + fine-location FE and (month × property type) FE:
17 +
18 + log P ~ log_fa + fa_missing + C(age_bin) | loc_fine + mo_type
19 +
20 +- Linking: the published province path per type is extended by the **mean
21 + splice** — the average (in logs) over every feasible overlap position of
22 + the growth implied by the new window. Movement and window splices are also
23 + returned as sensitivity diagnostics. The spliced history never revises.
24 +
25 +- Residuals for the hierarchy come from :func:`fullpool_residualize` — a
26 + single Huber-robust full-pool regression — NOT from the rolling windows.
27 + Rolling location FE re-absorb each cell's slow drift window after window,
28 + which empties the residuals of exactly the regional divergence stage 2
29 + must measure (verified against repeat sales: rolling-window residuals
30 + collapse Québec City condo's +20% divergence to ~0). Fixed-β full-pool
31 + residuals carry it. Since the SAME RTD anchor is added to every cell of a
32 + type, the published family stays internally consistent.
33 +
34 +- Diagnostics: the log-floor-area coefficient per window (parameter-drift
35 + evidence motivating RTD over a fixed-β full pool).
36 +"""
37 +
38 +from __future__ import annotations
39 +
40 +from dataclasses import dataclass
41 +
42 +import numpy as np
43 +import pandas as pd
44 +import polars as pl
45 +import pyfixest as pf
46 +
47 +from qwhpi.config import PROPERTY_TYPES
48 +
49 +WINDOW = 13 # months per estimation window
50 +HUBER_K = 1.345 # 95% Gaussian efficiency
51 +N_IRLS = 2 # reweighting passes after the initial OLS fit
52 +
53 +STRUCTURAL_RHS = "log_fa + fa_missing + C(age_bin)" # buildingType excluded: time-correlated missingness (features.py)
54 +
55 +
56 +@dataclass
57 +class RTDResult:
58 + """Stage-1 output of the rolling-time-dummy estimation."""
59 +
60 + months: list[str] # continuous monthly grid
61 + paths: dict[str, np.ndarray] # type -> mean-spliced log path
62 + paths_movement: dict[str, np.ndarray] # sensitivity: movement splice
63 + paths_window: dict[str, np.ndarray] # sensitivity: window splice
64 + residuals: pl.DataFrame # id, month, propertyType, region_code,
65 + # geo_code, municipality, resid
66 + drift: pl.DataFrame # window_end, beta_log_fa, n, r2
67 + sigma2: dict[str, float] # type -> robust residual variance
68 +
69 +
70 +def month_grid(months_present: list[str]) -> list[str]:
71 + """Continuous YYYY-MM grid between the first and last observed month."""
72 + first, last = min(months_present), max(months_present)
73 + y, m = int(first[:4]), int(first[5:7])
74 + out = []
75 + while True:
76 + cur = f"{y:04d}-{m:02d}"
77 + out.append(cur)
78 + if cur == last:
79 + return out
80 + m += 1
81 + if m == 13:
82 + y, m = y + 1, 1
83 +
84 +
85 +def _fit_window(pdf: pd.DataFrame) -> tuple[pf.estimation.models.feols_.Feols, pd.Series]:
86 + """Huber-IRLS fit of the window hedonic; returns (fit, final weights)."""
87 + pdf = pdf.copy()
88 + pdf["_w"] = 1.0
89 + fml = f"log_amount ~ {STRUCTURAL_RHS} | loc_fine + mo_type"
90 + fit = pf.feols(fml, data=pdf)
91 + for _ in range(N_IRLS):
92 + yhat = np.asarray(fit.predict(newdata=pdf), dtype=float)
93 + r = pdf["log_amount"].to_numpy() - yhat
94 + r = np.where(np.isfinite(r), r, 0.0)
95 + s = 1.4826 * np.median(np.abs(r - np.median(r))) or 1.0
96 + u = np.abs(r) / s
97 + pdf["_w"] = np.minimum(1.0, HUBER_K / np.maximum(u, 1e-9))
98 + fit = pf.feols(fml, data=pdf, weights="_w")
99 + return fit, pdf["_w"]
100 +
101 +
102 +def _window_deltas(fit) -> dict[tuple[str, str], float]:
103 + """(month, type) -> time-dummy log effect from the window's fixef."""
104 + fx = fit.fixef()
105 + key = next(k for k in fx if "mo_type" in k)
106 + out = {}
107 + for name, val in fx[key].items():
108 + mo, ptype = name.split("|")
109 + out[(mo, ptype)] = float(val)
110 + return out
111 +
112 +
113 +def local_time_dummy(
114 + sub: pl.DataFrame,
115 + grid: list[str],
116 +) -> tuple[np.ndarray, np.ndarray, float] | None:
117 + """Direct Huber-robust monthly time-dummy for ONE liquid cell.
118 +
119 + Own β, own location FE, own month effects — the estimator that matched
120 + repeat sales and stratified matched-cell medians in the v2 arbitration
121 + (deviation-from-pooled-surface compresses strong local divergences).
122 + Returns (log path aligned to grid, monthly n, robust residual variance),
123 + or None if estimation is impossible.
124 +
125 + Full-pool per cell (not windowed): within-cell β drift is second-order,
126 + and cell-level revisions are tracked by the vintage framework.
127 + """
128 + pdf = sub.select("log_amount", "month", "log_fa", "fa_missing",
129 + "age_bin", "loc_fine").to_pandas()
130 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
131 + pdf["mo_type"] = pdf["month"] # cell-level time FE = plain month
132 + if pdf["month"].nunique() < len(grid) * 0.9:
133 + return None
134 + try:
135 + fit, _ = _fit_window(pdf)
136 + except Exception:
137 + return None
138 + fx = fit.fixef()
139 + key = next(k for k in fx if "mo_type" in k)
140 + d = {m: float(v) for m, v in fx[key].items()}
141 + path = np.array([d.get(m, np.nan) for m in grid])
142 + if np.isnan(path).any():
143 + idx = np.arange(len(path))
144 + ok = np.isfinite(path)
145 + if ok.sum() < len(grid) * 0.9:
146 + return None
147 + path = np.interp(idx, idx[ok], path[ok])
148 +
149 + yhat = np.asarray(fit.predict(newdata=pdf), dtype=float)
150 + resid = pdf["log_amount"].to_numpy() - yhat
151 + resid = resid[np.isfinite(resid)]
152 + sigma2 = float(np.var(resid)) if resid.size else 0.09
153 + counts = sub.group_by("month").agg(pl.len().alias("n"))
154 + cmap = dict(counts.iter_rows())
155 + n = np.array([cmap.get(m, 0) for m in grid], dtype=float)
156 + return path, n, sigma2
157 +
158 +
159 +def fullpool_residualize(feat: pl.DataFrame) -> pl.DataFrame:
160 + """Huber-robust full-pool monthly regression → residuals for stage 2.
161 +
162 + Fixed β over the pool (with month×type FE and location FE) so the slow
163 + relative drift of each geography survives in the residual time path —
164 + the property the hierarchical deviations need (see module docstring).
165 + """
166 + cols = ["id", "log_amount", "month", "propertyType", "log_fa", "fa_missing",
167 + "age_bin", "building_type", "loc_fine", "region_code", "geo_code",
168 + "municipality"]
169 + pdf = feat.select(cols).with_columns(
170 + pl.concat_str([pl.col("month"), pl.lit("|"), pl.col("propertyType")])
171 + .alias("mo_type")
172 + ).to_pandas()
173 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
174 +
175 + fit, _ = _fit_window(pdf)
176 + yhat = np.asarray(fit.predict(newdata=pdf), dtype=float)
177 + pdf["resid"] = pdf["log_amount"].to_numpy() - yhat
178 + return pl.from_pandas(
179 + pdf[["id", "month", "propertyType", "region_code", "geo_code",
180 + "municipality", "resid"]]
181 + ).filter(pl.col("resid").is_not_nan() & pl.col("resid").is_not_null())
182 +
183 +
184 +def run_rtd(feat: pl.DataFrame, verbose: bool = True) -> RTDResult:
185 + """Run the full RTD stage 1 over the research sample features.
186 +
187 + ``feat`` needs: id, log_amount, month (YYYY-MM), propertyType, log_fa,
188 + fa_missing, age_bin, building_type, loc_fine, region_code, geo_code,
189 + municipality.
190 + """
191 + months = month_grid(sorted(feat["month"].unique().to_list()))
192 + T = len(months)
193 + pos = {m: i for i, m in enumerate(months)}
194 +
195 + cols = ["id", "log_amount", "month", "propertyType", "log_fa", "fa_missing",
196 + "age_bin", "building_type", "loc_fine", "region_code", "geo_code",
197 + "municipality"]
198 + base = feat.select(cols).with_columns(
199 + pl.concat_str([pl.col("month"), pl.lit("|"), pl.col("propertyType")])
200 + .alias("mo_type")
201 + )
202 +
203 + # Spliced log paths per type; initialized by the first window.
204 + paths = {t: np.full(T, np.nan) for t in PROPERTY_TYPES}
205 + paths_mv = {t: np.full(T, np.nan) for t in PROPERTY_TYPES}
206 + paths_wd = {t: np.full(T, np.nan) for t in PROPERTY_TYPES}
207 + resid_frames: list[pl.DataFrame] = []
208 + drift_rows: list[dict] = []
209 +
210 + n_windows = T - WINDOW + 1
211 + for w in range(n_windows):
212 + w_months = months[w:w + WINDOW]
213 + newest = w_months[-1]
214 + sub = base.filter(pl.col("month").is_in(w_months))
215 + pdf = sub.to_pandas()
216 + pdf["fa_missing"] = pdf["fa_missing"].astype(int)
217 +
218 + fit, _ = _fit_window(pdf)
219 + deltas = _window_deltas(fit)
220 + drift_rows.append({
221 + "window_end": newest,
222 + "beta_log_fa": float(fit.coef().get("log_fa", np.nan)),
223 + "n": int(fit._N),
224 + "r2": float(fit._r2),
225 + })
226 +
227 + # ---- splice per type ---- #
228 + for t in PROPERTY_TYPES:
229 + d = np.array([deltas.get((m, t), np.nan) for m in w_months])
230 + # interpolate the rare missing (singleton-dropped) window levels
231 + if np.isnan(d).any():
232 + idx = np.arange(len(d))
233 + ok = np.isfinite(d)
234 + if ok.sum() >= 2:
235 + d = np.interp(idx, idx[ok], d[ok])
236 + else:
237 + d = np.nan_to_num(d)
238 + if w == 0:
239 + # first window sets the initial 13 levels (its own normalization)
240 + for j, m in enumerate(w_months):
241 + v = d[j] - d[0]
242 + paths[t][pos[m]] = v
243 + paths_mv[t][pos[m]] = v
244 + paths_wd[t][pos[m]] = v
245 + else:
246 + i_new = pos[newest]
247 + # movement splice: last within-window growth
248 + paths_mv[t][i_new] = paths_mv[t][i_new - 1] + (d[-1] - d[-2])
249 + # window splice: growth across the full window
250 + paths_wd[t][i_new] = paths_wd[t][i_new - WINDOW + 1] + (d[-1] - d[0])
251 + # mean splice: average over every overlap position
252 + links = [
253 + paths[t][pos[m]] + (d[-1] - d[j])
254 + for j, m in enumerate(w_months[:-1])
255 + ]
256 + paths[t][i_new] = float(np.mean(links))
257 +
258 + # ---- real-time residuals ---- #
259 + target_months = w_months if w == 0 else [newest]
260 + rows = sub.filter(pl.col("month").is_in(target_months))
261 + rpdf = rows.to_pandas()
262 + rpdf["fa_missing"] = rpdf["fa_missing"].astype(int)
263 + yhat = np.asarray(fit.predict(newdata=rpdf), dtype=float)
264 + rpdf["resid"] = rpdf["log_amount"].to_numpy() - yhat
265 + keep = pl.from_pandas(
266 + rpdf[["id", "month", "propertyType", "region_code", "geo_code",
267 + "municipality", "resid"]]
268 + ).filter(pl.col("resid").is_not_nan() & pl.col("resid").is_not_null())
269 + resid_frames.append(keep)
270 +
271 + if verbose and (w % 10 == 0 or w == n_windows - 1):
272 + print(f" window {w + 1}/{n_windows} (end {newest}) "
273 + f"n={drift_rows[-1]['n']:,} r2={drift_rows[-1]['r2']:.3f} "
274 + f"beta_fa={drift_rows[-1]['beta_log_fa']:.3f}")
275 +
276 + residuals = pl.concat(resid_frames)
277 + sigma2 = {
278 + t: float(residuals.filter(pl.col("propertyType") == t)["resid"].var())
279 + for t in PROPERTY_TYPES
280 + }
281 + return RTDResult(
282 + months=months,
283 + paths=paths,
284 + paths_movement=paths_mv,
285 + paths_window=paths_wd,
286 + residuals=residuals,
287 + drift=pl.DataFrame(drift_rows),
288 + sigma2=sigma2,
289 + )
added engine/src/qwhpi/seasonal.py +47 −0
@@ -0,0 +1,47 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/seasonal.py
6 +# Purpose : Stable-seasonality test — QWHPI publishes NSA unless stable
7 +# weekly seasonality is demonstrated (CLAUDE.md §4.8).
8 +# =============================================================================
9 +"""Seasonality assessment.
10 +
11 +Test: regress weekly log-changes of a series on annual-cycle harmonics
12 +(sin/cos at 1–3 cycles/year) and F-test their joint significance. Stable
13 +seasonality additionally requires the harmonic fit to be consistent across
14 +years (coefficient stability). Until both hold, all series are NSA — the
15 +published index is never silently adjusted.
16 +"""
17 +
18 +from __future__ import annotations
19 +
20 +import numpy as np
21 +from scipy import stats
22 +
23 +
24 +def seasonality_test(log_index: np.ndarray, n_harmonics: int = 3) -> dict:
25 + """F-test of annual harmonics on weekly log-changes."""
26 + dy = np.diff(log_index)
27 + dy = dy[np.isfinite(dy)]
28 + t = np.arange(dy.shape[0])
29 + X = [np.ones_like(t, dtype=float)]
30 + for k in range(1, n_harmonics + 1):
31 + X.append(np.sin(2 * np.pi * k * t / 52.18))
32 + X.append(np.cos(2 * np.pi * k * t / 52.18))
33 + X = np.column_stack(X)
34 + beta, res, *_ = np.linalg.lstsq(X, dy, rcond=None)
35 + fitted = X @ beta
36 + rss1 = float(np.sum((dy - fitted) ** 2))
37 + rss0 = float(np.sum((dy - dy.mean()) ** 2))
38 + q = X.shape[1] - 1
39 + dof = dy.shape[0] - X.shape[1]
40 + f = ((rss0 - rss1) / q) / (rss1 / dof) if rss1 > 0 else np.inf
41 + p = 1.0 - stats.f.cdf(f, q, dof)
42 + return {
43 + "f_stat": round(float(f), 3),
44 + "p_value": round(float(p), 5),
45 + "seasonal_r2": round(1.0 - rss1 / rss0, 4) if rss0 > 0 else 0.0,
46 + "significant_5pct": bool(p < 0.05),
47 + }
added engine/src/qwhpi/state_space.py +123 −0
@@ -0,0 +1,123 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/state_space.py
6 +# Purpose : Local-level Kalman filter/smoother with heteroskedastic weekly
7 +# observation noise — the shrinkage engine for thin cells.
8 +# =============================================================================
9 +"""Heteroskedastic local-level state-space model.
10 +
11 +For one cell (geography × property type) we observe the weekly mean
12 +quality-adjusted residual ȳ_t (deviation of the cell from its parent path):
13 +
14 + ȳ_t = d_t + ε_t, ε_t ~ N(0, σ² / n_t) (observation)
15 + d_t = d_{t-1} + η_t, η_t ~ N(0, τ²) (latent deviation)
16 +
17 +- σ² is the within-cell transaction-level residual variance (estimated from
18 + the data), so weeks with more transactions speak with more weight.
19 +- τ² (the deviation's innovation variance) is estimated by maximizing the
20 + prediction-error decomposition log-likelihood.
21 +- Weeks with n_t = 0 are pure predictions (no update) — the grid is never
22 + dropped.
23 +
24 +Outputs both the one-sided *filtered* path (real-time, no lookahead — powers
25 +``index_smoothed``) and the two-sided *smoothed* path (research use), with
26 +posterior variances and an interpretable shrinkage weight
27 +(Kalman gain: share of the week's estimate coming from local data).
28 +"""
29 +
30 +from __future__ import annotations
31 +
32 +from dataclasses import dataclass
33 +
34 +import numpy as np
35 +from scipy.optimize import minimize_scalar
36 +
37 +
38 +@dataclass
39 +class LocalLevelFit:
40 + """Filtered + smoothed latent deviation path for one cell."""
41 +
42 + filtered: np.ndarray # E[d_t | y_1..t]
43 + filtered_var: np.ndarray
44 + smoothed: np.ndarray # E[d_t | y_1..T]
45 + smoothed_var: np.ndarray
46 + gain: np.ndarray # Kalman gain in [0,1]; 0 where n_t = 0
47 + tau2: float
48 + sigma2: float
49 + loglik: float
50 +
51 +
52 +def _filter(y: np.ndarray, r: np.ndarray, tau2: float,
53 + d0: float = 0.0, p0: float = 1.0):
54 + """Kalman filter for the local level model. r[t]=obs variance (inf if none)."""
55 + T = y.shape[0]
56 + a = np.empty(T) # filtered mean
57 + p = np.empty(T) # filtered variance
58 + a_pred = np.empty(T)
59 + p_pred = np.empty(T)
60 + gain = np.zeros(T)
61 + ll = 0.0
62 + at, pt = d0, p0
63 + for t in range(T):
64 + # predict
65 + at_p, pt_p = at, pt + tau2
66 + a_pred[t], p_pred[t] = at_p, pt_p
67 + if np.isfinite(r[t]):
68 + f = pt_p + r[t]
69 + v = y[t] - at_p
70 + k = pt_p / f
71 + at = at_p + k * v
72 + pt = pt_p * (1.0 - k)
73 + gain[t] = k
74 + ll += -0.5 * (np.log(2.0 * np.pi * f) + v * v / f)
75 + else:
76 + at, pt = at_p, pt_p
77 + a[t], p[t] = at, pt
78 + return a, p, a_pred, p_pred, gain, ll
79 +
80 +
81 +def _smooth(a, p, a_pred, p_pred, tau2):
82 + """Rauch-Tung-Striebel smoother."""
83 + T = a.shape[0]
84 + s = a.copy()
85 + sv = p.copy()
86 + for t in range(T - 2, -1, -1):
87 + c = p[t] / p_pred[t + 1] if p_pred[t + 1] > 0 else 0.0
88 + s[t] = a[t] + c * (s[t + 1] - a_pred[t + 1])
89 + sv[t] = p[t] + c * c * (sv[t + 1] - p_pred[t + 1])
90 + return s, np.maximum(sv, 0.0)
91 +
92 +
93 +def fit_local_level(
94 + y: np.ndarray,
95 + n: np.ndarray,
96 + sigma2: float,
97 + diffuse_p0: float = 0.5,
98 +) -> LocalLevelFit:
99 + """Fit the heteroskedastic local-level model to one cell.
100 +
101 + Parameters
102 + ----------
103 + y : weekly mean residual (NaN where no transactions)
104 + n : weekly transaction counts (0 where none)
105 + sigma2 : transaction-level residual variance for the cell
106 + """
107 + y = np.asarray(y, dtype=float)
108 + n = np.asarray(n, dtype=float)
109 + r = np.where(n > 0, sigma2 / np.maximum(n, 1.0), np.inf)
110 + yy = np.where(np.isfinite(r), np.nan_to_num(y), 0.0)
111 +
112 + def negll(log_tau2: float) -> float:
113 + return -_filter(yy, r, np.exp(log_tau2), 0.0, diffuse_p0)[5]
114 +
115 + # τ² for weekly log deviations plausibly lies in [1e-9, 1e-2].
116 + opt = minimize_scalar(negll, bounds=(np.log(1e-9), np.log(1e-2)),
117 + method="bounded")
118 + tau2 = float(np.exp(opt.x))
119 + a, p, a_pred, p_pred, gain, ll = _filter(yy, r, tau2, 0.0, diffuse_p0)
120 + s, sv = _smooth(a, p, a_pred, p_pred, tau2)
121 + return LocalLevelFit(filtered=a, filtered_var=p, smoothed=s,
122 + smoothed_var=sv, gain=gain, tau2=tau2,
123 + sigma2=sigma2, loglik=ll)
added engine/src/qwhpi/uncertainty.py +60 −0
@@ -0,0 +1,60 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/uncertainty.py
6 +# Purpose : Week-cluster bootstrap for validating the analytic CIs of the
7 +# hierarchical index on selected cells.
8 +# =============================================================================
9 +"""Uncertainty quantification.
10 +
11 +The published 95% bands combine the province-path delta-method variance and
12 +the Kalman posterior variance (see hierarchy.py). This module validates that
13 +approximation by a week-cluster bootstrap of stage-1 residuals: resample
14 +transactions within each week (preserving weekly sample sizes), refit the
15 +cell's local-level model, and compare the bootstrap dispersion of the
16 +smoothed path with the analytic posterior SD. The downsampling experiment
17 +(scripts/06) provides the complementary coverage check.
18 +"""
19 +
20 +from __future__ import annotations
21 +
22 +import numpy as np
23 +import polars as pl
24 +
25 +from qwhpi.hierarchy import cell_deviation
26 +
27 +
28 +def bootstrap_cell(
29 + cell_resid: pl.DataFrame,
30 + grid: list[str],
31 + sigma2: float,
32 + n_boot: int = 200,
33 + seed: int = 7,
34 +) -> dict:
35 + """Bootstrap the smoothed deviation path of one cell.
36 +
37 + ``cell_resid`` columns: week_str, resid.
38 + Returns mean analytic SD vs bootstrap SD and their ratio.
39 + """
40 + rng = np.random.default_rng(seed)
41 + base = cell_deviation(cell_resid.rename({"resid": "dev"}).select("week_str", "dev"),
42 + grid, sigma2)
43 + paths = np.empty((n_boot, len(grid)))
44 + pdf = cell_resid.to_pandas()
45 + grouped = {k: g["resid"].to_numpy() for k, g in pdf.groupby("week_str")}
46 + for b in range(n_boot):
47 + rows_w, rows_v = [], []
48 + for wk, vals in grouped.items():
49 + draw = vals[rng.integers(0, len(vals), len(vals))]
50 + rows_w.extend([wk] * len(draw))
51 + rows_v.extend(draw.tolist())
52 + boot = pl.DataFrame({"week_str": rows_w, "dev": rows_v})
53 + paths[b] = cell_deviation(boot, grid, sigma2).fit.smoothed
54 + boot_sd = paths.std(axis=0)
55 + analytic_sd = np.sqrt(base.fit.smoothed_var)
56 + return {
57 + "mean_analytic_sd": float(analytic_sd.mean()),
58 + "mean_bootstrap_sd": float(boot_sd.mean()),
59 + "sd_ratio_boot_over_analytic": float(boot_sd.mean() / max(analytic_sd.mean(), 1e-12)),
60 + }
added engine/src/qwhpi/vintages.py +75 −0
@@ -0,0 +1,75 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/src/qwhpi/vintages.py
6 +# Purpose : Vintage framework — first_release preservation and per-run
7 +# vintage snapshots for revision analysis.
8 +# =============================================================================
9 +"""Vintage management (CLAUDE.md §4.8).
10 +
11 +Every pipeline run publishes a ``data_vintage`` (run date). This module:
12 +- snapshots the canonical table under
13 + ``data/processed/vintages/data_vintage=<date>/qwhpi_weekly.parquet``;
14 +- maintains ``first_release.parquet``: the first value ever published for
15 + each (geography_id, property_type, week) — never overwritten;
16 +- computes revision statistics (current vintage vs first release).
17 +"""
18 +
19 +from __future__ import annotations
20 +
21 +import polars as pl
22 +
23 +from qwhpi.config import PROCESSED_DIR
24 +
25 +VINTAGE_DIR = PROCESSED_DIR / "vintages"
26 +FIRST_RELEASE = PROCESSED_DIR / "first_release.parquet"
27 +
28 +KEY = ["geography_id", "property_type", "week"]
29 +
30 +
31 +def snapshot(canonical: pl.DataFrame, data_vintage: str) -> None:
32 + out_dir = VINTAGE_DIR / f"data_vintage={data_vintage}"
33 + out_dir.mkdir(parents=True, exist_ok=True)
34 + canonical.write_parquet(out_dir / "qwhpi_weekly.parquet")
35 +
36 +
37 +def update_first_release(canonical: pl.DataFrame, data_vintage: str) -> pl.DataFrame:
38 + """Append newly-seen (cell, week) rows to the first-release store."""
39 + fresh = canonical.select(
40 + *KEY,
41 + pl.col("index").alias("first_release_index"),
42 + pl.col("index_smoothed").alias("first_release_index_smoothed"),
43 + pl.lit(data_vintage).alias("first_release_vintage"),
44 + )
45 + if FIRST_RELEASE.exists():
46 + existing = pl.read_parquet(FIRST_RELEASE)
47 + new_rows = fresh.join(existing.select(KEY), on=KEY, how="anti")
48 + merged = pl.concat([existing, new_rows])
49 + else:
50 + merged = fresh
51 + merged.write_parquet(FIRST_RELEASE)
52 + return merged
53 +
54 +
55 +def revision_stats(canonical: pl.DataFrame) -> pl.DataFrame | None:
56 + """Current vintage vs first release, per cell (needs >=2 vintages)."""
57 + if not FIRST_RELEASE.exists():
58 + return None
59 + fr = pl.read_parquet(FIRST_RELEASE)
60 + j = canonical.join(fr, on=KEY, how="inner").filter(
61 + pl.col("first_release_vintage") != pl.col("data_vintage"))
62 + if j.height == 0:
63 + return None
64 + return (
65 + j.with_columns(
66 + ((pl.col("index_smoothed") / pl.col("first_release_index_smoothed") - 1) * 100)
67 + .alias("revision_pct")
68 + )
69 + .group_by(["geography_id", "property_type"])
70 + .agg(
71 + pl.col("revision_pct").abs().mean().round(3).alias("mean_abs_revision_pct"),
72 + pl.col("revision_pct").abs().max().round(3).alias("max_abs_revision_pct"),
73 + pl.len().alias("n_revised_weeks"),
74 + )
75 + )
added engine/tests/test_features_clean.py +89 −0
@@ -0,0 +1,89 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/tests/test_features_clean.py
6 +# Purpose : Unit tests for cleaning flags and feature engineering.
7 +# =============================================================================
8 +"""Cleaning + feature unit tests on synthetic rows."""
9 +
10 +from __future__ import annotations
11 +
12 +import datetime as dt
13 +import sys
14 +from pathlib import Path
15 +
16 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
17 +
18 +import polars as pl
19 +
20 +from qwhpi.clean import add_flags
21 +from qwhpi.features import build_features
22 +
23 +
24 +def synthetic(n: int = 8) -> tuple[pl.DataFrame, pl.DataFrame]:
25 + tx = pl.DataFrame({
26 + "id": [f"t{i}" for i in range(n)],
27 + "date": [dt.date(2023, 1, 9)] * n,
28 + "amount": [300000.0, 300000.0, 55000.0, 20_000_000.0,
29 + 400000.0, 500000.0, 350000.0, 360000.0],
30 + "street": ["1 rue A", "1 rue A", "2 rue B", "3 rue C",
31 + "4 rue D", "5 rue E", "6 rue F", "7 rue G"],
32 + "zipCode": ["G1A 1A1"] * n,
33 + "city": ["X"] * n,
34 + "lat": [46.8] * n,
35 + "lng": [-71.2] * n,
36 + "propertyType": ["unifamilial"] * 6 + ["condo", "indéterminé"],
37 + "yearBuilt": [1990, 1990, 1990, 1990, 2030, None, 2000, 2000],
38 + "floorArea": [120.0, 120.0, 120.0, 120.0, 5000.0, None, 80.0, 80.0],
39 + "buildingType": ["single-story"] * 6 + [None, None],
40 + "previousValue": [250000.0] * n,
41 + "totalArValue": [280000.0, 280000.0, 280000.0, 280000.0,
42 + 380000.0, 100.0, 330000.0, 330000.0],
43 + "ownerType": ["physical_person"] * n,
44 + "week": [dt.date(2023, 1, 9)] * n,
45 + "log_amount": [12.6] * n,
46 + })
47 + geo = pl.DataFrame({
48 + "id": [f"t{i}" for i in range(n)],
49 + "geo_code": ["23027"] * n,
50 + "municipality": ["Québec"] * n,
51 + "munic_type": ["V"] * n,
52 + "mrc_code": ["230"] * n,
53 + "mrc": ["Québec"] * n,
54 + "region_code": ["03"] * n,
55 + "region": ["Capitale-Nationale"] * n,
56 + "join_method": ["within"] * n,
57 + })
58 + return tx, geo
59 +
60 +
61 +def test_flags():
62 + tx, geo = synthetic()
63 + df = add_flags(tx, geo)
64 + by_id = {r["id"]: r for r in df.iter_rows(named=True)}
65 + assert by_id["t1"]["duplicate_flag"] is True # exact dup of t0
66 + assert by_id["t0"]["duplicate_flag"] is False # first kept
67 + assert by_id["t3"]["exclude_reason"] == "price_above_10m"
68 + assert by_id["t5"]["nonmarket_ratio"] is True # ratio 5000x
69 + assert by_id["t4"]["yb_suspect"] is True # built 2030
70 + assert by_id["t4"]["fa_suspect"] is True # 5000 m2
71 + assert by_id["t4"]["yearBuilt"] is None # nulled, kept
72 + assert by_id["t7"]["is_undetermined_type"] is True
73 + # nothing is deleted
74 + assert df.height == tx.height
75 +
76 +
77 +def test_features_age_bins_and_imputation():
78 + tx, geo = synthetic()
79 + feat = build_features(add_flags(tx, geo))
80 + by_id = {r["id"]: r for r in feat.iter_rows(named=True)}
81 + assert by_id["t0"]["age_bin"] == "04_21-40" # age 33
82 + # missing yearBuilt -> conditional-median age imputation, NO missing bin
83 + # (time-correlated missingness rule — see features.py docstring)
84 + assert by_id["t5"]["yb_missing"] is True
85 + assert by_id["t5"]["age_filled"] is not None
86 + assert by_id["t5"]["age_bin"] != "99_missing"
87 + assert by_id["t5"]["fa_missing"] is True
88 + assert by_id["t5"]["floor_area_filled"] is not None # imputed
89 + assert by_id["t0"]["loc_fine"] == "G1A"
added engine/tests/test_smoke_pipeline.py +76 −0
@@ -0,0 +1,76 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/tests/test_smoke_pipeline.py
6 +# Purpose : CI smoke test — synthetic market through stage 1 + stage 2;
7 +# the recovered index must track the simulated price path.
8 +# =============================================================================
9 +"""End-to-end smoke test on a fully synthetic market (no data files)."""
10 +
11 +from __future__ import annotations
12 +
13 +import datetime as dt
14 +import sys
15 +from pathlib import Path
16 +
17 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
18 +
19 +import numpy as np
20 +import polars as pl
21 +
22 +from qwhpi.hierarchy import cell_deviation, stage1_residualize, week_grid
23 +
24 +RNG = np.random.default_rng(3)
25 +
26 +
27 +def simulate(n_weeks: int = 60, per_week: int = 150) -> tuple[pl.DataFrame, np.ndarray]:
28 + """Synthetic province with a known weekly log price path."""
29 + path = np.cumsum(RNG.normal(0.005, 0.003, n_weeks)) # clear boom
30 + rows = []
31 + start = dt.date(2021, 1, 4)
32 + for w in range(n_weeks):
33 + week = start + dt.timedelta(weeks=w)
34 + for i in range(per_week):
35 + fa = float(np.exp(RNG.normal(4.7, 0.35)))
36 + age_yrs = int(RNG.integers(0, 90))
37 + ptype = ["unifamilial", "condo", "plex"][int(RNG.integers(0, 3))]
38 + base = 12.0 + 0.6 * (np.log(fa) - 4.7) - 0.002 * age_yrs \
39 + + {"unifamilial": 0.0, "condo": -0.15, "plex": 0.2}[ptype]
40 + lp = base + path[w] + RNG.normal(0, 0.25)
41 + rows.append({
42 + "id": f"{w}-{i}",
43 + "log_amount": lp,
44 + "week_str": week.isoformat(),
45 + "propertyType": ptype,
46 + "log_fa": np.log(fa),
47 + "fa_missing": False,
48 + "age_bin": f"bin{age_yrs // 20}",
49 + "building_type": "single-story",
50 + "loc_fine": f"Z{int(RNG.integers(0, 6))}",
51 + "region_code": "03" if RNG.random() < 0.5 else "06",
52 + "geo_code": "23027",
53 + "municipality": "Québec",
54 + })
55 + return pl.DataFrame(rows), path
56 +
57 +
58 +def test_pipeline_recovers_simulated_path():
59 + df, truth = simulate()
60 + s1 = stage1_residualize(df)
61 + # province path per type ~ truth (up to a constant)
62 + uni = s1.paths.filter(pl.col("property_type") == "unifamilial").sort("week")
63 + est = uni["delta"].to_numpy()
64 + est = est - est.mean()
65 + tr = truth[-len(est):] - truth[-len(est):].mean()
66 + corr = np.corrcoef(est, tr)[0, 1]
67 + assert corr > 0.9, f"stage-1 path corr {corr:.3f}"
68 +
69 + # stage 2: a region with no true deviation should stay near zero
70 + grid = week_grid(s1.residuals)
71 + cell = s1.residuals.filter(
72 + (pl.col("region_code") == "03")
73 + & (pl.col("propertyType") == "unifamilial")).rename({"resid": "dev"})
74 + dev = cell_deviation(cell.select("week_str", "dev"), grid,
75 + float(cell["dev"].var()))
76 + assert np.abs(dev.fit.smoothed).max() < 0.05
added engine/tests/test_state_space.py +60 −0
@@ -0,0 +1,60 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : engine/tests/test_state_space.py
6 +# Purpose : Unit tests for the heteroskedastic local-level Kalman engine.
7 +# =============================================================================
8 +"""State-space unit tests."""
9 +
10 +from __future__ import annotations
11 +
12 +import sys
13 +from pathlib import Path
14 +
15 +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
16 +
17 +import numpy as np
18 +
19 +from qwhpi.state_space import fit_local_level
20 +
21 +RNG = np.random.default_rng(0)
22 +
23 +
24 +def test_recovers_constant_signal():
25 + T, n = 150, 30
26 + y = 0.05 + RNG.normal(0, 0.2 / np.sqrt(n), T)
27 + fit = fit_local_level(y, np.full(T, n), sigma2=0.04)
28 + assert abs(fit.smoothed[T // 2] - 0.05) < 0.02
29 + assert fit.tau2 < 1e-4 # constant state -> tiny innovation variance
30 +
31 +
32 +def test_tracks_drifting_signal():
33 + T, n = 200, 50
34 + truth = np.cumsum(RNG.normal(0, 0.01, T))
35 + y = truth + RNG.normal(0, 0.3 / np.sqrt(n), T)
36 + fit = fit_local_level(y, np.full(T, n), sigma2=0.09)
37 + rmse = np.sqrt(np.mean((fit.smoothed - truth) ** 2))
38 + assert rmse < 0.02
39 +
40 +
41 +def test_empty_weeks_are_predicted_not_dropped():
42 + T = 100
43 + n = np.full(T, 20.0)
44 + n[40:60] = 0 # 20-week gap
45 + truth = np.cumsum(RNG.normal(0, 0.02, T)) # real innovation -> tau2 > 0
46 + y = np.where(n > 0, truth + RNG.normal(0, 0.05, T), np.nan)
47 + fit = fit_local_level(y, n, sigma2=0.05)
48 + # gap weeks still get an estimate; uncertainty grows through the gap
49 + assert np.all(np.isfinite(fit.smoothed))
50 + assert fit.filtered_var[59] > fit.filtered_var[41] > fit.filtered_var[39]
51 + assert fit.gain[50] == 0.0 # no local data -> fully parent/past-driven
52 +
53 +
54 +def test_thin_weeks_shrink_more_than_liquid_weeks():
55 + T = 120
56 + n = np.full(T, 2.0)
57 + n[:60] = 200.0
58 + y = RNG.normal(0, 0.05, T)
59 + fit = fit_local_level(y, n, sigma2=0.09)
60 + assert fit.gain[:60].mean() > fit.gain[60:].mean()
added ops/ci/ci.yml +44 −0
@@ -0,0 +1,44 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : ops/ci/ci.yml
6 +# Purpose : CI pipeline — headers, lint, type-check, tests, smoke pipeline.
7 +# (Symlink/copy to .github/workflows/ci.yml when hosted on GitHub.)
8 +# =============================================================================
9 +
10 +name: qwhpi-ci
11 +on: [push, pull_request]
12 +
13 +jobs:
14 + checks:
15 + runs-on: ubuntu-latest
16 + steps:
17 + - uses: actions/checkout@v4
18 + - uses: actions/setup-python@v5
19 + with: { python-version: "3.12" }
20 + - uses: actions/setup-node@v4
21 + with: { node-version: "22" }
22 +
23 + - name: Install engine + api deps
24 + run: |
25 + pip install polars pandas numpy pyarrow scipy statsmodels pyfixest \
26 + fastapi uvicorn pydantic pytest httpx ruff mypy
27 +
28 + - name: Header check (mandatory)
29 + run: python scripts/check_headers.py
30 +
31 + - name: Ruff lint
32 + run: ruff check engine api scripts ops
33 +
34 + - name: Mypy (engine core)
35 + run: mypy engine/src/qwhpi --ignore-missing-imports || true
36 +
37 + - name: Engine unit tests
38 + run: cd engine && python -m pytest tests -q
39 +
40 + - name: Smoke pipeline (synthetic small sample)
41 + run: cd engine && python -m pytest tests/test_smoke_pipeline.py -q
42 +
43 + - name: TypeScript check
44 + run: cd web && npm ci && npx tsc --noEmit
added ops/scheduler/Dockerfile +13 −0
@@ -0,0 +1,13 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : ops/scheduler/Dockerfile
6 +# Purpose : Container image for the weekly refresh scheduler.
7 +# =============================================================================
8 +FROM python:3.12-slim
9 +RUN pip install --no-cache-dir polars numpy pyarrow scipy pandas pyfixest \
10 + "psycopg[binary]" schedule
11 +WORKDIR /workspace
12 +COPY refresh.py /opt/refresh.py
13 +CMD ["python", "/opt/refresh.py"]
added ops/scheduler/refresh.py +90 −0
@@ -0,0 +1,90 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : ops/scheduler/refresh.py
7 +# Purpose : Weekly refresh job — re-run the incremental pipeline every Monday
8 +# 06:00, write a run manifest, load the DB, log structured events.
9 +# =============================================================================
10 +"""Weekly refresh scheduler.
11 +
12 +Every Monday 06:00 (container time): ingest new rows → re-clean (append-only)
13 +→ re-estimate monthly indexes → new data_vintage → canonical rebuild → DB load.
14 +A JSON run manifest (input hash, row counts, timings) is written to
15 +``ops/scheduler/runs/``. The API's parquet cache invalidates automatically on
16 +file mtime; ETag values include the data vintage, so HTTP caches roll over.
17 +"""
18 +
19 +from __future__ import annotations
20 +
21 +import json
22 +import logging
23 +import subprocess
24 +import sys
25 +import time
26 +from datetime import datetime, timezone
27 +from pathlib import Path
28 +
29 +import schedule
30 +
31 +WORKSPACE = Path("/workspace")
32 +RUNS_DIR = WORKSPACE / "ops" / "scheduler" / "runs"
33 +
34 +logging.basicConfig(
35 + level=logging.INFO,
36 + format='{"ts":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}',
37 +)
38 +log = logging.getLogger("qwhpi.scheduler")
39 +
40 +STEPS = [
41 + "engine/scripts/03_clean.py",
42 + "engine/scripts/09_monthly.py",
43 + "engine/scripts/11_monthly_canonical.py",
44 +]
45 +
46 +
47 +def run_refresh() -> None:
48 + started = time.time()
49 + manifest: dict = {"started_at": datetime.now(timezone.utc).isoformat(),
50 + "steps": []}
51 + ok = True
52 + for step in STEPS:
53 + t0 = time.time()
54 + proc = subprocess.run([sys.executable, str(WORKSPACE / step)],
55 + cwd=WORKSPACE, capture_output=True, text=True)
56 + manifest["steps"].append({
57 + "step": step,
58 + "seconds": round(time.time() - t0, 1),
59 + "returncode": proc.returncode,
60 + })
61 + if proc.returncode != 0:
62 + log.error("step %s failed: %s", step, proc.stderr[-2000:])
63 + ok = False
64 + break
65 + log.info("step %s ok (%.0fs)", step, time.time() - t0)
66 +
67 + if ok:
68 + sys.path.insert(0, str(WORKSPACE / "engine" / "src"))
69 + try:
70 + from qwhpi.export import input_hash, load_database
71 + manifest["input_hash"] = input_hash()
72 + manifest["db_loaded"] = load_database()
73 + except Exception as exc: # noqa: BLE001
74 + log.error("db load failed: %s", exc)
75 + manifest["db_loaded"] = False
76 +
77 + manifest["ok"] = ok
78 + manifest["total_seconds"] = round(time.time() - started, 1)
79 + RUNS_DIR.mkdir(parents=True, exist_ok=True)
80 + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
81 + (RUNS_DIR / f"run_{stamp}.json").write_text(json.dumps(manifest, indent=2))
82 + log.info("refresh finished ok=%s in %.0fs", ok, manifest["total_seconds"])
83 +
84 +
85 +if __name__ == "__main__":
86 + log.info("scheduler up — weekly refresh Mondays 06:00")
87 + schedule.every().monday.at("06:00").do(run_refresh)
88 + while True:
89 + schedule.run_pending()
90 + time.sleep(60)
added outputs/figures/assessment_gap.png +0 −0

Binary file not shown.

added outputs/figures/big4_cities.png +0 −0

Binary file not shown.

added outputs/figures/condo_by_city.png +0 −0

Binary file not shown.

added outputs/figures/downsampling_stability.png +0 −0

Binary file not shown.

added outputs/figures/quebec_aggregate.png +0 −0

Binary file not shown.

added outputs/figures/raw_median_vs_hedonic.png +0 −0

Binary file not shown.

added outputs/figures/region_comparison.png +0 −0

Binary file not shown.

added outputs/figures/reliability_matrix.png +0 −0

Binary file not shown.

added outputs/figures/volumes.png +0 −0

Binary file not shown.

added outputs/figures/yoy_map.png +0 −0

Binary file not shown.

added outputs/reports/audit_report.md +127 −0
@@ -0,0 +1,127 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/audit_report.md
7 +Purpose : Step 1 data audit report (generated by engine/scripts/01_profile.py)
8 +=============================================================================
9 +-->
10 +
11 +# QWHPI Data Audit Report
12 +
13 +Author: Simon-Pierre Boucher — contact@spboucher.ai
14 +
15 +## 1. Schema and shape
16 +
17 +- Rows: 745,119
18 +- Columns: 17
19 +- Date range: 2021-01-04 -> 2026-07-27
20 +- Distinct Monday-labeled weeks with >=1 transaction: 291
21 +- Distinct cities (raw text field): 1,100
22 +- Distinct ids: 745,119 (duplicated ids: 0)
23 +
24 + -> table saved: outputs/tables/audit_schema.csv (17 rows)
25 +
26 +## 2. Missingness
27 +
28 + -> table saved: outputs/tables/audit_missingness.csv (17 rows)
29 +- `buildingType`: 283,206 missing (38.01%)
30 +- `ownerType`: 224,999 missing (30.2%)
31 +- `yearBuilt`: 137,315 missing (18.43%)
32 +- `floorArea`: 115,153 missing (15.45%)
33 +- `previousValue`: 78,581 missing (10.55%)
34 +- `zipCode`: 75,369 missing (10.12%)
35 +- `totalArValue`: 63,160 missing (8.48%)
36 +- `street`: 6,688 missing (0.9%)
37 +
38 +Sentinel / degenerate values (not encoded as null):
39 + -> table saved: outputs/tables/audit_sentinel_values.csv (11 rows)
40 +- amount <= 0: 0
41 +- amount in (1, 2): 0
42 +- amount < 5000: 0
43 +- floorArea <= 0: 0
44 +- floorArea > 2000 m2: 375
45 +- yearBuilt < 1600: 0
46 +- yearBuilt > 2026: 0
47 +- yearBuilt == 0: 0
48 +- previousValue <= 0: 1,640
49 +- totalArValue <= 0: 17
50 +- lat/lng outside Quebec bbox (44.9-62.7N, -79.8--57W): 0
51 +
52 +## 3. Categorical distributions
53 +
54 +### propertyType
55 + -> table saved: outputs/tables/audit_dist_propertyType.csv (4 rows)
56 +- unifamilial: 443,078 (59.46%)
57 +- indéterminé: 107,656 (14.45%)
58 +- condo: 100,218 (13.45%)
59 +- plex: 94,167 (12.64%)
60 +
61 +### buildingType
62 + -> table saved: outputs/tables/audit_dist_buildingType.csv (6 rows)
63 +- None: 283,206 (38.01%)
64 +- single-story: 205,924 (27.64%)
65 +- full-story: 188,584 (25.31%)
66 +- mansard: 52,808 (7.09%)
67 +- split-level: 8,743 (1.17%)
68 +- single-wide: 5,854 (0.79%)
69 +
70 +### ownerType
71 + -> table saved: outputs/tables/audit_dist_ownerType.csv (4 rows)
72 +- physical_person: 496,404 (66.62%)
73 +- None: 224,999 (30.2%)
74 +- legal_person: 22,351 (3.0%)
75 +- mixed: 1,365 (0.18%)
76 +
77 +## 4. Amount descriptives by propertyType (amount > 0)
78 +
79 + -> table saved: outputs/tables/audit_amount_by_type.csv (4 rows)
80 +- unifamilial: n=443,078 | p01=$60,000 | median=$378,000 | p99=$1,635,000 | max=$345,000,000
81 +- indéterminé: n=107,656 | p01=$52,185 | median=$319,252 | p99=$1,400,000 | max=$75,000,000
82 +- condo: n=100,218 | p01=$103,500 | median=$360,000 | p99=$1,300,000 | max=$18,750,000
83 +- plex: n=94,167 | p01=$75,000 | median=$470,577 | p99=$1,710,000 | max=$42,000,000
84 + -> table saved: outputs/tables/audit_characteristics_by_type.csv (4 rows)
85 +
86 +## 5. Duplicate scan
87 +
88 +- Exact duplicates ignoring id: 665
89 +- Same street+city+date (candidate multi-unit or true dupes): 10,925
90 +- Same street+city+date+amount (strong duplicate candidates): 784
91 +- Addresses (street+city) with >=2 distinct sale dates: 87,787
92 +
93 +## 6. Weekly coverage and liquidity
94 +
95 +- Continuous Monday grid: 291 weeks (2021-01-04 -> 2026-07-27)
96 +- Zero-transaction weeks on the grid: 0
97 +- Median tx/week (province): 2,351
98 +- Min tx/week: 35 | Max: 7,845
99 +- Last 6 weeks (partial-week check): 2026-06-22=4226, 2026-06-29=3581, 2026-07-06=2934, 2026-07-13=2973, 2026-07-20=2197, 2026-07-27=318
100 + -> table saved: outputs/tables/audit_weekly_volume_province.csv (291 rows)
101 + -> table saved: outputs/tables/audit_weekly_liquidity_topcities.csv (100 rows)
102 + -> table saved: outputs/tables/audit_liquidity_matrix.csv (25 rows)
103 +
104 +Median weekly transactions, top cities (rows) x type (cols): see audit_liquidity_matrix.csv
105 +- {'city': 'Québec', 'condo': 41.5, 'indéterminé': 6.0, 'plex': 21.0, 'unifamilial': 66.0}
106 +- {'city': 'Montréal', 'condo': 145.0, 'indéterminé': 39.0, 'plex': 80.0, 'unifamilial': 64.0}
107 +- {'city': 'Gatineau', 'condo': 7.0, 'indéterminé': 12.0, 'plex': 12.0, 'unifamilial': 54.0}
108 +- {'city': 'Laval', 'condo': 21.0, 'indéterminé': 8.0, 'plex': 8.0, 'unifamilial': 54.0}
109 +- {'city': 'Sherbrooke', 'condo': 2.0, 'indéterminé': 6.0, 'plex': 7.0, 'unifamilial': 29.0}
110 +- {'city': 'Longueuil', 'condo': 15.0, 'indéterminé': 10.0, 'plex': 9.0, 'unifamilial': 28.0}
111 +- {'city': 'Lévis', 'condo': 4.0, 'indéterminé': 6.0, 'plex': 5.0, 'unifamilial': 26.0}
112 +- {'city': 'Saguenay', 'condo': 2.0, 'indéterminé': 4.0, 'plex': 8.0, 'unifamilial': 26.0}
113 +- {'city': 'Trois-Rivières', 'condo': 2.0, 'indéterminé': 5.0, 'plex': 8.0, 'unifamilial': 23.0}
114 +- {'city': 'Terrebonne', 'condo': 3.0, 'indéterminé': 6.0, 'plex': 3.0, 'unifamilial': 22.0}
115 +- {'city': 'Saint-Jean-sur-Richelieu', 'condo': 2.0, 'indéterminé': 4.0, 'plex': 3.0, 'unifamilial': 18.0}
116 +- {'city': 'Drummondville', 'condo': 1.0, 'indéterminé': 3.0, 'plex': 4.0, 'unifamilial': 16.0}
117 +
118 +- Top 50 city strings cover 61.4% of transactions
119 + -> table saved: outputs/tables/audit_city_top200.csv (200 rows)
120 +
121 +## 7. `indéterminé` property type — preliminary look
122 +
123 +- Count: 107,656 (14.4%)
124 +- Median amount: $319,252 (vs unifamilial $378,000)
125 +- floorArea missing: 95.6% | yearBuilt missing: 95.2% | buildingType missing: 97.9%
126 +- Top buildingType values within indéterminé: None=105,377, single-story=1,089, mansard=601, full-story=557, single-wide=26, split-level=6
127 +- ownerType within indéterminé: None=73,911, physical_person=20,620, legal_person=12,657, mixed=468
added outputs/reports/cleaning_report.md +42 −0
@@ -0,0 +1,42 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/cleaning_report.md
7 +Purpose : Step 3 cleaning report (generated by engine/scripts/03_clean.py)
8 +=============================================================================
9 +-->
10 +
11 +## Cleaning report
12 +
13 +- Input rows: 745,119 (all retained; exclusions are flags)
14 +- Screens: ratio band [0.2, 5.0] on amount/totalArValue; price ceiling $10,000,000; duplicate flags; floorArea sanitation [20.0, 2000.0] m².
15 +- Provider pre-filter verified: no transaction below $50,000 exists in the raw file.
16 +
17 +### Exclusion table (outputs/tables/exclusions.csv)
18 +- assessment_ratio_outside_band: 24,976 (3.352%)
19 +- exact_duplicate: 665 (0.089%)
20 +- addr_date_amount_duplicate: 107 (0.014%)
21 +- price_above_10m: 46 (0.006%)
22 +- TOTAL_EXCLUDED: 25,794 (3.462%)
23 +- RETAINED_IN_RESEARCH_SAMPLE: 719,325 (96.538%)
24 +
25 +### Characteristic sanitation (row retained, value nulled)
26 +- floorArea outside [20.0, 2000.0] m²: 576
27 +- yearBuilt > sale year + 1: 19
28 +
29 +### Research sample
30 +- Clean rows incl. indéterminé: 719,325
31 +- Headline sample (3 determined types): 626,127 (84.03% of raw)
32 + - unifamilial: 439,118
33 + - condo: 94,843
34 + - plex: 92,166
35 +- Weeks with >=1 research-sample transaction: 291 / 291
36 +- Median weekly research-sample volume: 1,999
37 +
38 +### Missingness in headline research sample
39 +- floorArea: 1.85%
40 +- yearBuilt: 5.4%
41 +- buildingType: 27.61%
42 +- ownerType: 23.7%
added outputs/reports/geography_report.md +47 −0
@@ -0,0 +1,47 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/geography_report.md
7 +Purpose : Step 2 geography join + validation report (from 02_geography.py)
8 +=============================================================================
9 +-->
10 +
11 +## Geography spatial join (SDA 1/20k, version V2026-07)
12 +
13 +- Join computed/loaded in 2.1s for 745,119 transactions
14 +- join_method=within: 745,119 (100.000%)
15 +- Distinct regions matched: 17 | municipalities: 1,125
16 +
17 +Transactions by administrative region:
18 +- 16 Montérégie: 134,113
19 +- 06 Montréal: 116,682
20 +- 15 Laurentides: 77,374
21 +- 03 Capitale-Nationale: 73,093
22 +- 14 Lanaudière: 57,064
23 +- 05 Estrie: 52,133
24 +- 07 Outaouais: 44,087
25 +- 12 Chaudière-Appalaches: 42,729
26 +- 13 Laval: 28,651
27 +- 02 Saguenay–Lac-Saint-Jean: 26,725
28 +- 04 Mauricie: 26,218
29 +- 17 Centre-du-Québec: 22,995
30 +- 01 Bas-Saint-Laurent: 17,245
31 +- 08 Abitibi-Témiscamingue: 12,273
32 +- 11 Gaspésie–Îles-de-la-Madeleine: 6,452
33 +- 09 Côte-Nord: 5,911
34 +- 10 Nord-du-Québec: 1,374
35 +
36 +### Validation: joined municipality vs raw `city` text
37 +- Name agreement (normalized, containment-tolerant): 99.92%
38 +- Mismatching pairs saved: outputs/tables/geo_city_mismatch_top300.csv (7 distinct pairs, 618 transactions)
39 +
40 +Top 15 mismatch pairs (city text -> joined municipality):
41 +- 'Saint-Bruno' -> 'Hébertville' (Saguenay–Lac-Saint-Jean): 292
42 +- 'Courcelles-Saint-Évariste' -> 'Courcelles–Saint-Évariste' (Chaudière-Appalaches): 140
43 +- 'Saint-Félix-de-Dalquier' -> 'Amos' (Abitibi-Témiscamingue): 95
44 +- 'Saint-Onésime-d'Ixworth' -> 'La Pocatière' (Bas-Saint-Laurent): 61
45 +- 'Passes-Dangereuses' -> 'Sainte-Élisabeth-de-Proulx' (Saguenay–Lac-Saint-Jean): 16
46 +- 'Saint-Guy' -> 'Lac-des-Aigles' (Bas-Saint-Laurent): 13
47 +- 'TNO aquatique de la MRC d'Argenteuil' -> 'Grenville-sur-la-Rouge' (Laurentides): 1
added outputs/reports/hierarchical_report.md +79 −0
@@ -0,0 +1,79 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/hierarchical_report.md
7 +Purpose : Step 5 — hierarchical weekly model: architecture, benchmarks,
8 + and the pooled-β vs city-β arbitration by repeat sales.
9 +=============================================================================
10 +-->
11 +
12 +# Hierarchical weekly model — design record
13 +
14 +## Architecture chosen (two-stage)
15 +
16 +1. **Stage 1 — pooled residualization** (pyfixest, n=625,882, R²=0.605, ~3s):
17 + `log(P) ~ log(floorArea) + fa_missing + age bins + building type | FSA FE + (week × type) FE`.
18 + The absorbed week×type effects are the province-level weekly paths per
19 + property type; residuals are quality- and location-adjusted deviations.
20 +2. **Stage 2 — heteroskedastic local-level Kalman** per cell:
21 + weekly mean residual observed with variance σ²/n_t; latent deviation is a
22 + random walk with innovation τ² estimated by MLE per cell. Region cells
23 + deviate from the province path; municipality cells deviate from the
24 + smoothed region path. Weeks with zero transactions are pure predictions —
25 + the grid is never dropped.
26 +
27 +Published variants: `index` (raw weekly), `index_smoothed` (one-sided filter,
28 +real-time), `index_research` (two-sided smoother). All three share one
29 +rebasing constant (2021 avg = 100), so they are directly comparable.
30 +
31 +## Shrinkage behaviour (outputs/tables/hierarchical_summary.csv)
32 +
33 +- Trois-Rivières condo (median 1 tx/week): raw weekly SD 30.4% → smoothed 4.2%.
34 +- Drummondville condo (median 0 tx/week): still produces a continuous path,
35 + ~98% parent-driven (shrinkage weight).
36 +- Liquid cells (province types, ~300–1,500 tx/week) are data-dominated.
37 +
38 +## Benchmark: two-stage vs unified city-specific time-dummy (Step 4 baseline)
39 +
40 +Liquid province cells agree almost exactly (corr 0.9996, Québec unifamilial).
41 +Montréal condo exposed a genuine specification fork: by July 2026 the
42 +two-stage index reads 126.9 vs 118.6 for the city-β baseline.
43 +
44 +**Arbitration by pseudo repeat-sales** (BMN, yearly dummies; condo pairs
45 +matched on street + floorArea + yearBuilt because condo addresses carry no
46 +unit numbers — 6,079 Montréal condo pairs, ≥90 days between sales):
47 +
48 +| Measure (2026 level, 2021=100) | Montréal condo | Province condo | Relative |
49 +|--------------------------------------------|---------------:|---------------:|---------:|
50 +| Pseudo repeat-sales (composition-free) | 120.4 | 132.0 | −8.8% |
51 +| Two-stage hierarchical (pooled β) | 126.9 | 138.2 | −8.2% |
52 +| City-specific time-dummy baseline | 118.6 | ~139 | −14% |
53 +
54 +The two-stage model reproduces the composition-free *relative* path almost
55 +exactly; the city-β baseline overstates Montréal's divergence. Hedonic levels
56 +sit a few percent above repeat-sales, the standard direction (depreciation /
57 +renovation bias in repeat pairs). **Decision: the two-stage hierarchical
58 +model is the headline architecture.** The unified per-cell time-dummy remains
59 +as a robustness spec.
60 +
61 +## Compute cost
62 +
63 +Full stage 1 + 112 cells of stage 2 run in under one minute on a laptop —
64 +weekly refresh is trivially cheap. A Bayesian multilevel variant (PyMC) was
65 +rejected for the headline: equivalent shrinkage behaviour at orders of
66 +magnitude more compute, and harder vintage reproducibility.
67 +
68 +## Known caveats (tracked)
69 +
70 +- `se_log` combines the province-path delta-method variance and the Kalman
71 + posterior variance; block-bootstrap validation of CI coverage is part of
72 + Step 6.
73 +- The Kalman gain-based `shrinkage_weight` measures how much of a week's
74 + *movement* comes from the parent path; reliability grades therefore weigh
75 + n / effective N / SE primarily (Step 7).
76 +- Montréal plex R² is low (0.20) in any spec — unobserved revenue
77 + characteristics; reliability grading must reflect this.
78 +
79 +Author: Simon-Pierre Boucher — contact@spboucher.ai
added outputs/reports/methodology_v2_research.md +84 −0
@@ -0,0 +1,84 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/methodology_v2_research.md
7 +Purpose : Web-researched design record for the v2 (monthly, robust) index.
8 +=============================================================================
9 +-->
10 +
11 +# QHPI v2 — monthly, robust: research record and design decisions
12 +
13 +## Why revisit v1
14 +
15 +The v1 weekly index estimated ONE pooled hedonic over 2021–2026 (fixed
16 +coefficients for 5.5 years) and published weekly points whose thin-cell noise
17 +had to be repaired by heavy shrinkage. Two structural weaknesses:
18 +
19 +1. **Fixed hedonic coefficients over the full pool.** The implicit prices of
20 + floor area, age and location are not constant across a boom, a rate-hike
21 + correction and a recovery. The literature flags the fixed-parameter
22 + restriction as the main drawback of the full-pool time-dummy method.
23 +2. **Full-sample estimation revises history on every run** — each refresh
24 + re-estimates every β, so past index points move.
25 +
26 +## What the literature recommends
27 +
28 +- **Rolling-Time-Dummy (RTD)**: estimate the time-dummy hedonic on a rolling
29 + window of recent periods only, then link (splice) each new estimate onto
30 + the existing series. Used by official HPIs; requires less data, adapts to
31 + parameter drift, and is "well suited for computing higher frequency HPIs".
32 + Hill, Scholz, Shimizu & Steurer (2022, *Journal of Official Statistics*
33 + 38(1):127–151) treat the three design choices: window length, linking
34 + variant (movement / window / half / **mean splice**), and low-volume
35 + robustness.
36 +- **Chained/spliced time dummies produce a revision-free series** and relax
37 + the fixed-parameter assumption (Eurostat/IMF/OECD *RPPI Handbook*, ch. 5).
38 +- **Mean splice** (geometric mean over all feasible link positions) is the
39 + robust default in the splicing literature (Diewert & Fox 2021 for
40 + multilateral updating): no single anomalous overlap period drives the link.
41 +- **Robust estimation**: OLS hedonics chase influential observations;
42 + M-estimation (Huber weights) or documented influence-trimming is standard
43 + practice for official filters (cf. IMF WP/16/213, MNB HPI outlier
44 + procedures).
45 +- **Monthly frequency**: weekly cells below the metro level are too thin for
46 + publication-grade points without aggressive shrinkage; monthly multiplies
47 + cell counts ~4.3× and is the modal frequency of official HPIs.
48 +
49 +Sources:
50 +- Hill, Scholz, Shimizu & Steurer (2022) — https://journals.sagepub.com/doi/abs/10.2478/jos-2022-0007
51 +- Eurostat et al., Handbook on Residential Property Prices Indices — https://www.oecd.org/content/dam/oecd/en/publications/reports/2013/04/handbook-on-residential-property-price-indices_g1g2e251/9789264197183-en.pdf
52 +- RPPI Handbook ch.5 (hedonic methods) — https://www.elibrary.imf.org/display/book/9789279259845/ch005.xml
53 +- IMF WP/16/213, "How to Better Measure Hedonic RPPIs" — https://www.imf.org/external/pubs/ft/wp/2016/wp16213.pdf
54 +- IMF RPPI Practical Guide — https://www.imf.org/-/media/files/data/guides/rppi/rppi-guide.pdf
55 +
56 +## v2 design (implemented)
57 +
58 +1. **Frequency: monthly** (labels `YYYY-MM`, continuous grid, trailing month
59 + flagged partial while registrations arrive).
60 +2. **Stage 1 — RTD, 13-month rolling windows, monthly step.** Within each
61 + window: hedonic `log P ~ log_fa + fa_missing + age bins + building type
62 + | FSA FE + (month × type) FE`, estimated by **iteratively reweighted least
63 + squares with Huber weights** (k = 1.345 on MAD-standardized residuals,
64 + 2 reweighting passes) — outliers are downweighted inside the regression
65 + instead of silently trimmed. A 13-month window spans every calendar month
66 + once (seasonal balance) and holds ~120k transactions.
67 +3. **Linking: mean splice.** For each new window the province path per type
68 + is extended by the geometric mean of the growth implied at every feasible
69 + overlap position. Movement and window splices are computed as published
70 + sensitivity diagnostics. The published history never revises.
71 +4. **Stage 2 — unchanged hierarchy, monthly grid.** Each transaction's
72 + quality-adjusted residual comes from the window in which its month is the
73 + NEWEST month (fully real-time, β local in time). Cell deviations
74 + (region → municipality) follow the heteroskedastic local-level Kalman
75 + model; thin months shrink toward the parent path.
76 +5. **Growth horizons**: 1m, 3m, 6m, 12m (YoY). Reliability grades
77 + recalibrated by a monthly downsampling experiment.
78 +6. **New diagnostics**: hedonic coefficient drift across windows (the
79 + empirical justification for RTD), RTD vs full-pool comparison, splice
80 + variant sensitivity.
81 +7. Cleaning screens, geography, `indéterminé` exclusion, fixed-share "all"
82 + composition, assessment-gap module: unchanged from v1 (already validated).
83 +
84 +Author: Simon-Pierre Boucher — contact@spboucher.ai
added outputs/reports/validation_report.md +43 −0
@@ -0,0 +1,43 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/validation_report.md
7 +Purpose : Step 6 validation report (generated by 06_validation.py)
8 +=============================================================================
9 +-->
10 +
11 +# QWHPI validation report
12 +
13 +## 1. Repeat-sales (BMN) vs hedonic index
14 +
15 +- quebec/unifamilial: Δcorr=0.691, end levels RS=161.5 vs hedonic=157.9
16 +- quebec/condo: Δcorr=0.322, end levels RS=133.8 vs hedonic=140.0
17 +- quebec/plex: Δcorr=0.454, end levels RS=149.6 vs hedonic=158.5
18 +- montreal/condo: Δcorr=0.000, end levels RS=121.8 vs hedonic=128.7
19 +- montreal/unifamilial: Δcorr=0.264, end levels RS=125.6 vs hedonic=132.8
20 +- quebec-city/unifamilial: Δcorr=0.073, end levels RS=165.7 vs hedonic=166.0
21 +
22 +## 2. Downsampling experiment — Montréal condo lab
23 +
24 +- 100 tx/wk: RMSE=0.317% bias=-0.046% vol_ratio=1.051 turn_agree=0.861 CI95_cov=1.0
25 +- 50 tx/wk: RMSE=0.636% bias=-0.177% vol_ratio=0.987 turn_agree=0.799 CI95_cov=0.992
26 +- 25 tx/wk: RMSE=0.799% bias=0.002% vol_ratio=0.762 turn_agree=0.771 CI95_cov=0.977
27 +- 15 tx/wk: RMSE=1.15% bias=-0.154% vol_ratio=0.449 turn_agree=0.778 CI95_cov=0.881
28 +- 10 tx/wk: RMSE=1.361% bias=-0.18% vol_ratio=0.358 turn_agree=0.735 CI95_cov=0.828
29 +- 5 tx/wk: RMSE=1.633% bias=0.189% vol_ratio=0.457 turn_agree=0.685 CI95_cov=0.856
30 +
31 +Reading: RMSE vs the full-sample (~145 tx/wk) smoothed path; CI coverage of the deviation posterior; turning points on 4-week MA.
32 +
33 +## 3. Composition-shock simulation (province unifamilial)
34 +
35 +- Shock: drop 70% of below-median floorArea sales in 2024-H1 (12,406 of 439,118 rows removed)
36 +- Raw weekly median moves by +11.74% in shocked weeks (must move)
37 +- Hedonic time-dummy moves by -0.74% (must stay ~0)
38 +
39 +## 4. Weekly vs monthly (same methodology)
40 +
41 +- quebec/unifamilial: weekly signal SD=3.665%, noise SD=0.0%, monthly ΔSD=3.198%, real-time revision SD=0.0%
42 +- quebec/condo: weekly signal SD=3.778%, noise SD=0.0%, monthly ΔSD=2.993%, real-time revision SD=0.0%
43 +- montreal/condo: weekly signal SD=3.782%, noise SD=3.583%, monthly ΔSD=3.031%, real-time revision SD=3.999%
added outputs/reports/validation_v2_report.md +42 −0
@@ -0,0 +1,42 @@
1 +<!--
2 +=============================================================================
3 +QWHPI — Quebec Weekly Housing Price Index
4 +Author : Simon-Pierre Boucher
5 +Contact : contact@spboucher.ai
6 +File : outputs/reports/validation_v2_report.md
7 +Purpose : v2.1 monthly validation report (10_monthly_validation.py)
8 +=============================================================================
9 +-->
10 +
11 +# QHPI v2.1 validation report (monthly, robust)
12 +
13 +## 1. Repeat sales (BMN, unit-signature pairs) vs published index
14 +
15 +- quebec/unifamilial: Δcorr=0.749, écart final hedonic−RS = -4.6%
16 +- quebec/condo: Δcorr=0.368, écart final hedonic−RS = -3.2%
17 +- quebec/plex: Δcorr=0.436, écart final hedonic−RS = -3.4%
18 +- montreal/condo: Δcorr=0.238, écart final hedonic−RS = -1.2%
19 +- montreal/unifamilial: Δcorr=0.262, écart final hedonic−RS = -8.1%
20 +- quebec-city/condo: Δcorr=0.084, écart final hedonic−RS = +1.1%
21 +- quebec-city/unifamilial: Δcorr=0.333, écart final hedonic−RS = -3.4%
22 +
23 +## 2. Downsampling (Montréal condo, ~620 tx/month)
24 +
25 +- 300 tx/mois: RMSE direct=1.01% | hiérarchique=1.3%
26 +- 150 tx/mois: RMSE direct=1.2% | hiérarchique=1.37%
27 +- 75 tx/mois: RMSE direct=1.49% | hiérarchique=1.31%
28 +- 40 tx/mois: RMSE direct=1.85% | hiérarchique=1.45%
29 +- 20 tx/mois: RMSE direct=2.09% | hiérarchique=1.69%
30 +- 10 tx/mois: RMSE direct=2.59% | hiérarchique=2.21%
31 +
32 +Lecture: en RMSE pur (référence = direct plein échantillon, cellule à faible divergence) le croisement se situe vers 75–150 tx/mois. Mais le RMSE du hiérarchique contient un biais de compression qui explose sur les cellules fortement divergentes (Québec-ville condo: ~8–10 pts, cf. arbitrage RS/stratifié) alors que l'erreur du direct est du bruit pur (~1.9% à 40 tx/mois), réduit par le lisseur d'état. LIQUID_MIN=40 échange donc un bruit borné contre un biais non borné.
33 +
34 +## 3. Choc de composition (province unifamilial, mensuel)
35 +- Médiane brute: +12.27% dans les mois choqués (doit bouger)
36 +- Hédonique: -1.02% (doit rester ~0)
37 +
38 +## 4. Diagnostics de robustesse
39 +- Dérive β(log surface) sur 55 fenêtres: 0.556 → 0.586 (amplitude 5.2% de la moyenne) — justifie le RTD contre le pool fixe.
40 +- Saisonnalité quebec/all: F=0.444 p=0.77608 → non significative (NSA)
41 +- Saisonnalité quebec/unifamilial: F=0.417 p=0.79551 → non significative (NSA)
42 +- Saisonnalité montreal/condo: F=0.749 p=0.56243 → non significative (NSA)
added outputs/tables/audit_amount_by_type.csv +5 −0
@@ -0,0 +1,5 @@
1 +propertyType,n,p01,p05,p25,median,p75,p95,p99,max,mean
2 +unifamilial,443078,60000.0,100000.0,240000.0,378000.0,540000.0,925000.0,1635000.0,345000000.0,434952.0
3 +indéterminé,107656,52185.0,74733.0,210000.0,319252.0,443500.0,800000.0,1400000.0,75000000.0,371907.0
4 +condo,100218,103500.0,169000.0,275000.0,360000.0,469000.0,772000.0,1300000.0,18750000.0,405822.0
5 +plex,94167,75000.0,132000.0,285000.0,470577.0,725000.0,1156000.0,1710000.0,42000000.0,544999.0
added outputs/tables/audit_characteristics_by_type.csv +5 −0
@@ -0,0 +1,5 @@
1 +propertyType,n,floorArea_med,floorArea_p99,yearBuilt_med,yearBuilt_min,floorArea_miss_pct,yearBuilt_miss_pct,buildingType_miss_pct
2 +unifamilial,443078,111.7,344.4,1981.0,1634,1.43,6.01,23.85
3 +indéterminé,107656,128.4,6552.8,1990.0,1734,95.62,95.24,97.88
4 +condo,100218,87.6,188.2,2006.0,1725,2.58,1.56,38.59
5 +plex,94167,200.7,861.8,1958.0,1600,3.48,7.01,35.55
added outputs/tables/audit_city_top200.csv +201 −0
@@ -0,0 +1,201 @@
1 +city,len
2 +Montréal,103061
3 +Québec,47820
4 +Laval,28651
5 +Gatineau,27357
6 +Longueuil,19851
7 +Lévis,14499
8 +Sherbrooke,14350
9 +Saguenay,12896
10 +Trois-Rivières,12224
11 +Terrebonne,10839
12 +Brossard,9310
13 +Saint-Jérôme,8614
14 +Saint-Jean-sur-Richelieu,8560
15 +Repentigny,7735
16 +Drummondville,7401
17 +Mirabel,7142
18 +Granby,6744
19 +Mascouche,5884
20 +Blainville,5549
21 +Shawinigan,5328
22 +Saint-Hyacinthe,4648
23 +Salaberry-de-Valleyfield,4576
24 +Rimouski,4277
25 +Victoriaville,4123
26 +Saint-Eustache,3998
27 +Boucherville,3885
28 +Vaudreuil-Dorion,3744
29 +Châteauguay,3730
30 +Magog,3482
31 +Sorel-Tracy,3444
32 +Chambly,3272
33 +Rouyn-Noranda,3265
34 +Saint-Lin–Laurentides,3067
35 +Val-d'Or,2948
36 +Alma,2925
37 +Sainte-Julie,2917
38 +Mont-Tremblant,2819
39 +Saint-Georges,2788
40 +Candiac,2746
41 +Dollard-des-Ormeaux,2599
42 +Beloeil,2526
43 +Sainte-Adèle,2517
44 +La Prairie,2515
45 +Saint-Constant,2508
46 +Sainte-Thérèse,2470
47 +Sainte-Marthe-sur-le-Lac,2470
48 +Bromont,2465
49 +Thetford Mines,2448
50 +Saint-Bruno-de-Montarville,2333
51 +Saint-Lambert,2329
52 +Sainte-Sophie,2321
53 +Pointe-Claire,2260
54 +L'Assomption,2250
55 +Val-des-Monts,2238
56 +Boisbriand,2226
57 +Mont-Saint-Hilaire,2184
58 +Saint-Sauveur,2148
59 +Saint-Hippolyte,2125
60 +Saint-Colomban,2048
61 +Saint-Augustin-de-Desmaures,1969
62 +Sept-Îles,1942
63 +Sainte-Julienne,1800
64 +Varennes,1798
65 +Rawdon,1774
66 +Saint-Lazare,1763
67 +Saint-Amable,1743
68 +Lavaltrie,1742
69 +Saint-Basile-le-Grand,1672
70 +Beauharnois,1579
71 +Cowansville,1577
72 +Rivière-du-Loup,1554
73 +Sainte-Catherine,1553
74 +Prévost,1552
75 +Bécancour,1508
76 +Baie-Comeau,1504
77 +Sainte-Agathe-des-Monts,1488
78 +Carignan,1479
79 +Saint-Raymond,1475
80 +Joliette,1470
81 +Lachute,1459
82 +Stoneham-et-Tewkesbury,1449
83 +Côte-Saint-Luc,1442
84 +La Pêche,1438
85 +Pont-Rouge,1438
86 +Contrecoeur,1416
87 +Sainte-Anne-des-Plaines,1415
88 +Deux-Montagnes,1392
89 +Mercier,1391
90 +Sainte-Brigitte-de-Laval,1383
91 +Sainte-Catherine-de-la-Jacques-Cartier,1368
92 +L'Ancienne-Lorette,1347
93 +Saint-Calixte,1341
94 +Saint-Zotique,1303
95 +Dolbeau-Mistassini,1302
96 +Saint-Donat,1280
97 +Pincourt,1276
98 +Chertsey,1269
99 +Saint-Apollinaire,1266
100 +Mont-Laurier,1240
101 +Dorval,1238
102 +Marieville,1235
103 +Beaconsfield,1225
104 +Mont-Royal,1209
105 +Cantley,1200
106 +Saint-Philippe,1194
107 +Rosemère,1183
108 +Matane,1156
109 +Brownsburg-Chatham,1125
110 +Bois-des-Filion,1121
111 +Westmount,1110
112 +Farnham,1110
113 +Sainte-Marie,1100
114 +Orford,1097
115 +Saint-Adolphe-d'Howard,1088
116 +Boischatel,1075
117 +Chelsea,1067
118 +Beaupré,1027
119 +Lac-Beauport,1024
120 +La Tuque,1014
121 +Kirkland,1005
122 +Les Îles-de-la-Madeleine,1005
123 +L'Île-Perrot,980
124 +Saint-Félicien,957
125 +Lac-Brome,956
126 +Otterburn Park,953
127 +Piedmont,951
128 +Notre-Dame-de-l'Île-Perrot,931
129 +Gaspé,929
130 +Lorraine,889
131 +Montmagny,872
132 +L'Épiphanie,861
133 +Saint-Rémi,854
134 +Sainte-Marguerite-du-Lac-Masson,853
135 +Roberval,849
136 +Shannon,848
137 +La Malbaie,844
138 +Notre-Dame-des-Prairies,844
139 +Amos,843
140 +Saint-Charles-Borromée,840
141 +Morin-Heights,816
142 +Saint-Ferréol-les-Neiges,815
143 +Waterloo,805
144 +Les Coteaux,804
145 +Saint-Honoré,799
146 +Val-David,796
147 +Donnacona,790
148 +Saint-Côme,786
149 +Rigaud,780
150 +Val-des-Sources,761
151 +Chibougamau,755
152 +Coaticook,752
153 +Saint-Félix-de-Valois,747
154 +Saint-Lambert-de-Lauzon,744
155 +L'Ange-Gardien,742
156 +Saint-Jean-de-Matha,740
157 +Acton Vale,735
158 +Saint-Joseph-du-Lac,724
159 +Sutton,717
160 +Shefford,717
161 +Saint-Paul,709
162 +Rivière-Rouge,707
163 +Roxton Pond,698
164 +Coteau-du-Lac,696
165 +Pointe-Calumet,693
166 +Mont-Blanc,679
167 +Lanoraie,672
168 +Charlemagne,672
169 +Delson,663
170 +Plessisville,663
171 +Saint-Alphonse-Rodriguez,661
172 +Saint-Denis-de-Brompton,658
173 +Les Cèdres,643
174 +Pontiac,640
175 +Hudson,625
176 +Sainte-Anne-des-Lacs,618
177 +Nicolet,618
178 +Saint-Pie,617
179 +Saint-Isidore,598
180 +Sainte-Anne-des-Monts,586
181 +Saint-Alexis-des-Monts,573
182 +Notre-Dame-du-Mont-Carmel,567
183 +Baie-Saint-Paul,564
184 +Château-Richer,562
185 +Louiseville,555
186 +Saint-Agapit,554
187 +Lac-Mégantic,551
188 +Saint-Henri,546
189 +Eastman,545
190 +Mont-Joli,543
191 +Saint-Ambroise,541
192 +La Sarre,539
193 +Saint-Damien,534
194 +Potton,531
195 +Saint-Jacques,529
196 +Princeville,528
197 +Saint-David-de-Falardeau,525
198 +Lac-Supérieur,518
199 +Saint-Césaire,516
200 +Mandeville,504
201 +Notre-Dame-du-Laus,500
added outputs/tables/audit_dist_buildingType.csv +7 −0
@@ -0,0 +1,7 @@
1 +buildingType,len,pct
2 +,283206,38.01
3 +single-story,205924,27.64
4 +full-story,188584,25.31
5 +mansard,52808,7.09
6 +split-level,8743,1.17
7 +single-wide,5854,0.79
added outputs/tables/audit_dist_ownerType.csv +5 −0
@@ -0,0 +1,5 @@
1 +ownerType,len,pct
2 +physical_person,496404,66.62
3 +,224999,30.2
4 +legal_person,22351,3.0
5 +mixed,1365,0.18
added outputs/tables/audit_dist_propertyType.csv +5 −0
@@ -0,0 +1,5 @@
1 +propertyType,len,pct
2 +unifamilial,443078,59.46
3 +indéterminé,107656,14.45
4 +condo,100218,13.45
5 +plex,94167,12.64
added outputs/tables/audit_liquidity_matrix.csv +26 −0
@@ -0,0 +1,26 @@
1 +city,condo,indéterminé,plex,unifamilial
2 +Québec,41.5,6.0,21.0,66.0
3 +Montréal,145.0,39.0,80.0,64.0
4 +Gatineau,7.0,12.0,12.0,54.0
5 +Laval,21.0,8.0,8.0,54.0
6 +Sherbrooke,2.0,6.0,7.0,29.0
7 +Longueuil,15.0,10.0,9.0,28.0
8 +Lévis,4.0,6.0,5.0,26.0
9 +Saguenay,2.0,4.0,8.0,26.0
10 +Trois-Rivières,2.0,5.0,8.0,23.0
11 +Terrebonne,3.0,6.0,3.0,22.0
12 +Saint-Jean-sur-Richelieu,2.0,4.0,3.0,18.0
13 +Drummondville,1.0,3.0,4.0,16.0
14 +Repentigny,5.0,2.0,2.0,16.0
15 +Saint-Jérôme,2.0,5.0,5.0,15.0
16 +Granby,2.0,3.0,3.0,13.0
17 +Blainville,1.0,3.0,2.0,12.0
18 +Brossard,5.0,11.0,1.0,12.0
19 +Mirabel,1.0,6.0,4.0,12.0
20 +Shawinigan,1.0,2.0,5.0,11.0
21 +Mascouche,2.0,5.0,2.0,10.0
22 +Rimouski,2.0,2.0,2.0,10.0
23 +Saint-Hyacinthe,1.0,3.0,3.0,9.0
24 +Salaberry-de-Valleyfield,1.0,2.0,3.0,9.0
25 +Victoriaville,1.0,2.0,2.0,9.0
26 +Saint-Eustache,2.0,2.0,1.0,8.0
added outputs/tables/audit_missingness.csv +18 −0
@@ -0,0 +1,18 @@
1 +column,n_missing,pct_missing
2 +buildingType,283206,38.01
3 +ownerType,224999,30.2
4 +yearBuilt,137315,18.43
5 +floorArea,115153,15.45
6 +previousValue,78581,10.55
7 +zipCode,75369,10.12
8 +totalArValue,63160,8.48
9 +street,6688,0.9
10 +id,0,0.0
11 +date,0,0.0
12 +amount,0,0.0
13 +city,0,0.0
14 +lat,0,0.0
15 +lng,0,0.0
16 +propertyType,0,0.0
17 +week,0,0.0
18 +log_amount,0,0.0
added outputs/tables/audit_schema.csv +18 −0
@@ -0,0 +1,18 @@
1 +column,dtype
2 +id,String
3 +date,Date
4 +amount,Float64
5 +street,String
6 +zipCode,String
7 +city,String
8 +lat,Float64
9 +lng,Float64
10 +propertyType,String
11 +yearBuilt,Int64
12 +floorArea,Float64
13 +buildingType,String
14 +previousValue,Float64
15 +totalArValue,Float64
16 +ownerType,String
17 +week,Date
18 +log_amount,Float64
added outputs/tables/audit_sentinel_values.csv +12 −0
@@ -0,0 +1,12 @@
1 +check,n
2 +amount <= 0,0
3 +"amount in (1, 2)",0
4 +amount < 5000,0
5 +floorArea <= 0,0
6 +floorArea > 2000 m2,375
7 +yearBuilt < 1600,0
8 +yearBuilt > 2026,0
9 +yearBuilt == 0,0
10 +previousValue <= 0,1640
11 +totalArValue <= 0,17
12 +"lat/lng outside Quebec bbox (44.9-62.7N, -79.8--57W)",0
added outputs/tables/audit_weekly_liquidity_topcities.csv +101 −0
@@ -0,0 +1,101 @@
1 +city,propertyType,median_wk,mean_wk,weeks_present,pct_weeks_present
2 +Blainville,condo,1.0,1.7,144,49.5
3 +Blainville,indéterminé,3.0,3.8,253,86.9
4 +Blainville,plex,2.0,2.5,234,80.4
5 +Blainville,unifamilial,12.0,13.2,284,97.6
6 +Brossard,condo,5.0,7.0,267,91.8
7 +Brossard,indéterminé,11.0,13.4,279,95.9
8 +Brossard,plex,1.0,1.5,128,44.0
9 +Brossard,unifamilial,12.0,12.3,285,97.9
10 +Drummondville,condo,1.0,1.3,107,36.8
11 +Drummondville,indéterminé,3.0,3.7,243,83.5
12 +Drummondville,plex,4.0,4.5,274,94.2
13 +Drummondville,unifamilial,16.0,18.1,285,97.9
14 +Gatineau,condo,7.0,9.1,281,96.6
15 +Gatineau,indéterminé,12.0,14.5,263,90.4
16 +Gatineau,plex,12.0,12.9,286,98.3
17 +Gatineau,unifamilial,54.0,60.2,288,99.0
18 +Granby,condo,2.0,2.6,207,71.1
19 +Granby,indéterminé,3.0,4.6,259,89.0
20 +Granby,plex,3.0,3.8,253,86.9
21 +Granby,unifamilial,13.0,14.3,283,97.3
22 +Laval,condo,21.0,21.9,288,99.0
23 +Laval,indéterminé,8.0,10.2,281,96.6
24 +Laval,plex,8.0,8.9,285,97.9
25 +Laval,unifamilial,54.0,58.7,289,99.3
26 +Longueuil,condo,15.0,16.7,286,98.3
27 +Longueuil,indéterminé,10.0,12.4,281,96.6
28 +Longueuil,plex,9.0,9.7,287,98.6
29 +Longueuil,unifamilial,28.0,30.6,288,99.0
30 +Lévis,condo,4.0,5.0,256,88.0
31 +Lévis,indéterminé,6.0,8.5,273,93.8
32 +Lévis,plex,5.0,5.5,279,95.9
33 +Lévis,unifamilial,26.0,32.8,286,98.3
34 +Mascouche,condo,2.0,3.3,232,79.7
35 +Mascouche,indéterminé,5.0,5.8,238,81.8
36 +Mascouche,plex,2.0,2.3,220,75.6
37 +Mascouche,unifamilial,10.0,11.3,287,98.6
38 +Mirabel,condo,1.0,1.5,105,36.1
39 +Mirabel,indéterminé,6.0,7.5,278,95.5
40 +Mirabel,plex,4.0,4.2,276,94.8
41 +Mirabel,unifamilial,12.0,13.0,287,98.6
42 +Montréal,condo,145.0,157.5,291,100.0
43 +Montréal,indéterminé,39.0,44.6,288,99.0
44 +Montréal,plex,80.0,85.7,291,100.0
45 +Montréal,unifamilial,64.0,67.3,289,99.3
46 +Québec,condo,41.5,51.6,288,99.0
47 +Québec,indéterminé,6.0,7.7,282,96.9
48 +Québec,plex,21.0,24.0,288,99.0
49 +Québec,unifamilial,66.0,82.9,288,99.0
50 +Repentigny,condo,5.0,6.0,282,96.9
51 +Repentigny,indéterminé,2.0,2.5,197,67.7
52 +Repentigny,plex,2.0,2.0,213,73.2
53 +Repentigny,unifamilial,16.0,17.7,291,100.0
54 +Rimouski,condo,2.0,2.0,176,60.5
55 +Rimouski,indéterminé,2.0,2.4,182,62.5
56 +Rimouski,plex,2.0,2.1,189,64.9
57 +Rimouski,unifamilial,10.0,11.0,281,96.6
58 +Saguenay,condo,2.0,2.1,183,62.9
59 +Saguenay,indéterminé,4.0,4.3,255,87.6
60 +Saguenay,plex,8.0,9.3,280,96.2
61 +Saguenay,unifamilial,26.0,31.2,282,96.9
62 +Saint-Eustache,condo,2.0,2.1,176,60.5
63 +Saint-Eustache,indéterminé,2.0,3.2,219,75.3
64 +Saint-Eustache,plex,1.0,1.7,189,64.9
65 +Saint-Eustache,unifamilial,8.0,9.2,282,96.9
66 +Saint-Hyacinthe,condo,1.0,1.1,16,5.5
67 +Saint-Hyacinthe,indéterminé,3.0,3.9,269,92.4
68 +Saint-Hyacinthe,plex,3.0,3.3,254,87.3
69 +Saint-Hyacinthe,unifamilial,9.0,9.7,282,96.9
70 +Saint-Jean-sur-Richelieu,condo,2.0,2.5,200,68.7
71 +Saint-Jean-sur-Richelieu,indéterminé,4.0,4.4,260,89.3
72 +Saint-Jean-sur-Richelieu,plex,3.0,3.9,270,92.8
73 +Saint-Jean-sur-Richelieu,unifamilial,18.0,20.3,289,99.3
74 +Saint-Jérôme,condo,2.0,3.5,223,76.6
75 +Saint-Jérôme,indéterminé,5.0,6.4,251,86.3
76 +Saint-Jérôme,plex,5.0,6.0,283,97.3
77 +Saint-Jérôme,unifamilial,15.0,15.8,286,98.3
78 +Salaberry-de-Valleyfield,condo,1.0,1.2,64,22.0
79 +Salaberry-de-Valleyfield,indéterminé,2.0,3.1,207,71.1
80 +Salaberry-de-Valleyfield,plex,3.0,3.6,266,91.4
81 +Salaberry-de-Valleyfield,unifamilial,9.0,10.3,282,96.9
82 +Shawinigan,condo,1.0,1.2,53,18.2
83 +Shawinigan,indéterminé,2.0,2.1,203,69.8
84 +Shawinigan,plex,5.0,5.3,282,96.9
85 +Shawinigan,unifamilial,11.0,11.8,284,97.6
86 +Sherbrooke,condo,2.0,2.3,223,76.6
87 +Sherbrooke,indéterminé,6.0,7.6,277,95.2
88 +Sherbrooke,plex,7.0,8.6,285,97.9
89 +Sherbrooke,unifamilial,29.0,32.2,289,99.3
90 +Terrebonne,condo,3.0,3.9,241,82.8
91 +Terrebonne,indéterminé,6.0,6.8,258,88.7
92 +Terrebonne,plex,3.0,3.5,266,91.4
93 +Terrebonne,unifamilial,22.0,24.8,291,100.0
94 +Trois-Rivières,condo,2.0,2.7,211,72.5
95 +Trois-Rivières,indéterminé,5.0,6.3,267,91.8
96 +Trois-Rivières,plex,8.0,9.1,284,97.6
97 +Trois-Rivières,unifamilial,23.0,25.6,288,99.0
98 +Victoriaville,condo,1.0,1.1,67,23.0
99 +Victoriaville,indéterminé,2.0,2.4,203,69.8
100 +Victoriaville,plex,2.0,2.4,223,76.6
101 +Victoriaville,unifamilial,9.0,10.7,282,96.9
added outputs/tables/audit_weekly_volume_province.csv +292 −0
@@ -0,0 +1,292 @@
1 +week,transactions
2 +2021-01-04,35
3 +2021-01-11,123
4 +2021-01-18,136
5 +2021-01-25,2184
6 +2021-02-01,2731
7 +2021-02-08,1902
8 +2021-02-15,2254
9 +2021-02-22,3000
10 +2021-03-01,3216
11 +2021-03-08,1998
12 +2021-03-15,2624
13 +2021-03-22,2549
14 +2021-03-29,3849
15 +2021-04-05,2975
16 +2021-04-12,3026
17 +2021-04-19,2707
18 +2021-04-26,5269
19 +2021-05-03,4437
20 +2021-05-10,3735
21 +2021-05-17,4097
22 +2021-05-24,4527
23 +2021-05-31,7821
24 +2021-06-07,4513
25 +2021-06-14,6317
26 +2021-06-21,5859
27 +2021-06-28,7845
28 +2021-07-05,4180
29 +2021-07-12,4916
30 +2021-07-19,2736
31 +2021-07-26,3060
32 +2021-08-02,2569
33 +2021-08-09,2480
34 +2021-08-16,2896
35 +2021-08-23,2904
36 +2021-08-30,4084
37 +2021-09-06,2044
38 +2021-09-13,2669
39 +2021-09-20,2510
40 +2021-09-27,3542
41 +2021-10-04,2806
42 +2021-10-11,2146
43 +2021-10-18,2482
44 +2021-10-25,3150
45 +2021-11-01,3292
46 +2021-11-08,2240
47 +2021-11-15,2711
48 +2021-11-22,2679
49 +2021-11-29,3970
50 +2021-12-06,2730
51 +2021-12-13,4302
52 +2021-12-20,2469
53 +2021-12-27,245
54 +2022-01-03,506
55 +2022-01-10,1725
56 +2022-01-17,2313
57 +2022-01-24,2308
58 +2022-01-31,2718
59 +2022-02-07,1693
60 +2022-02-14,1965
61 +2022-02-21,2206
62 +2022-02-28,2836
63 +2022-03-07,1712
64 +2022-03-14,2197
65 +2022-03-21,2023
66 +2022-03-28,3409
67 +2022-04-04,2476
68 +2022-04-11,2085
69 +2022-04-18,2437
70 +2022-04-25,3753
71 +2022-05-02,3756
72 +2022-05-09,2793
73 +2022-05-16,3247
74 +2022-05-23,3086
75 +2022-05-30,6092
76 +2022-06-06,3395
77 +2022-06-13,4812
78 +2022-06-20,4168
79 +2022-06-27,6074
80 +2022-07-04,4282
81 +2022-07-11,3874
82 +2022-07-18,3741
83 +2022-07-25,2633
84 +2022-08-01,2071
85 +2022-08-08,1691
86 +2022-08-15,2271
87 +2022-08-22,2345
88 +2022-08-29,3411
89 +2022-09-05,1822
90 +2022-09-12,2299
91 +2022-09-19,2023
92 +2022-09-26,2723
93 +2022-10-03,2396
94 +2022-10-10,1737
95 +2022-10-17,1988
96 +2022-10-24,2333
97 +2022-10-31,1829
98 +2022-11-07,1685
99 +2022-11-14,1982
100 +2022-11-21,1950
101 +2022-11-28,3063
102 +2022-12-05,2006
103 +2022-12-12,2865
104 +2022-12-19,2515
105 +2022-12-26,342
106 +2023-01-02,294
107 +2023-01-09,1080
108 +2023-01-16,1515
109 +2023-01-23,1321
110 +2023-01-30,1695
111 +2023-02-06,944
112 +2023-02-13,1189
113 +2023-02-20,1309
114 +2023-02-27,1732
115 +2023-03-06,1094
116 +2023-03-13,1447
117 +2023-03-20,1311
118 +2023-03-27,2215
119 +2023-04-03,1507
120 +2023-04-10,1522
121 +2023-04-17,1601
122 +2023-04-24,2438
123 +2023-05-01,2947
124 +2023-05-08,1709
125 +2023-05-15,2554
126 +2023-05-22,2156
127 +2023-05-29,4800
128 +2023-06-05,2669
129 +2023-06-12,3809
130 +2023-06-19,3132
131 +2023-06-26,4146
132 +2023-07-03,4099
133 +2023-07-10,2798
134 +2023-07-17,3088
135 +2023-07-24,2368
136 +2023-07-31,2428
137 +2023-08-07,1554
138 +2023-08-14,2147
139 +2023-08-21,2083
140 +2023-08-28,3160
141 +2023-09-04,1746
142 +2023-09-11,1937
143 +2023-09-18,1821
144 +2023-09-25,2522
145 +2023-10-02,2150
146 +2023-10-09,1500
147 +2023-10-16,1921
148 +2023-10-23,1919
149 +2023-10-30,2422
150 +2023-11-06,1554
151 +2023-11-13,1886
152 +2023-11-20,1668
153 +2023-11-27,2590
154 +2023-12-04,1914
155 +2023-12-11,2292
156 +2023-12-18,2420
157 +2023-12-25,306
158 +2024-01-01,234
159 +2024-01-08,897
160 +2024-01-15,1438
161 +2024-01-22,1251
162 +2024-01-29,1629
163 +2024-02-05,952
164 +2024-02-12,1155
165 +2024-02-19,1157
166 +2024-02-26,2044
167 +2024-03-04,1187
168 +2024-03-11,1373
169 +2024-03-18,1427
170 +2024-03-25,1975
171 +2024-04-01,2144
172 +2024-04-08,1583
173 +2024-04-15,1947
174 +2024-04-22,2095
175 +2024-04-29,3758
176 +2024-05-06,1960
177 +2024-05-13,2838
178 +2024-05-20,2266
179 +2024-05-27,4708
180 +2024-06-03,3941
181 +2024-06-10,3899
182 +2024-06-17,5488
183 +2024-06-24,5240
184 +2024-07-01,3085
185 +2024-07-08,2860
186 +2024-07-15,3386
187 +2024-07-22,2194
188 +2024-07-29,2708
189 +2024-08-05,1758
190 +2024-08-12,2247
191 +2024-08-19,2282
192 +2024-08-26,2988
193 +2024-09-02,2050
194 +2024-09-09,2071
195 +2024-09-16,2191
196 +2024-09-23,2464
197 +2024-09-30,2804
198 +2024-10-07,2089
199 +2024-10-14,1988
200 +2024-10-21,2117
201 +2024-10-28,3114
202 +2024-11-04,2215
203 +2024-11-11,2275
204 +2024-11-18,2374
205 +2024-11-25,3014
206 +2024-12-02,2860
207 +2024-12-09,2973
208 +2024-12-16,3825
209 +2024-12-23,480
210 +2024-12-30,236
211 +2025-01-06,901
212 +2025-01-13,2207
213 +2025-01-20,1838
214 +2025-01-27,2383
215 +2025-02-03,1715
216 +2025-02-10,1485
217 +2025-02-17,1678
218 +2025-02-24,2582
219 +2025-03-03,1720
220 +2025-03-10,1701
221 +2025-03-17,1844
222 +2025-03-24,2134
223 +2025-03-31,2931
224 +2025-04-07,1696
225 +2025-04-14,2080
226 +2025-04-21,2345
227 +2025-04-28,4301
228 +2025-05-05,2351
229 +2025-05-12,3181
230 +2025-05-19,2363
231 +2025-05-26,4558
232 +2025-06-02,4354
233 +2025-06-09,3876
234 +2025-06-16,5140
235 +2025-06-23,5016
236 +2025-06-30,4484
237 +2025-07-07,3318
238 +2025-07-14,3864
239 +2025-07-21,2453
240 +2025-07-28,2797
241 +2025-08-04,2099
242 +2025-08-11,2432
243 +2025-08-18,2569
244 +2025-08-25,3395
245 +2025-09-01,2337
246 +2025-09-08,2215
247 +2025-09-15,2577
248 +2025-09-22,2446
249 +2025-09-29,3270
250 +2025-10-06,2239
251 +2025-10-13,2230
252 +2025-10-20,2320
253 +2025-10-27,3221
254 +2025-11-03,2429
255 +2025-11-10,2295
256 +2025-11-17,2393
257 +2025-11-24,2967
258 +2025-12-01,3037
259 +2025-12-08,2928
260 +2025-12-15,3981
261 +2025-12-22,579
262 +2025-12-29,177
263 +2026-01-05,769
264 +2026-01-12,1943
265 +2026-01-19,1868
266 +2026-01-26,2211
267 +2026-02-02,1659
268 +2026-02-09,1391
269 +2026-02-16,1599
270 +2026-02-23,2155
271 +2026-03-02,1595
272 +2026-03-09,1522
273 +2026-03-16,1843
274 +2026-03-23,1895
275 +2026-03-30,2610
276 +2026-04-06,1709
277 +2026-04-13,2185
278 +2026-04-20,1997
279 +2026-04-27,3850
280 +2026-05-04,2503
281 +2026-05-11,2738
282 +2026-05-18,2224
283 +2026-05-25,3846
284 +2026-06-01,4494
285 +2026-06-08,3299
286 +2026-06-15,4687
287 +2026-06-22,4226
288 +2026-06-29,3581
289 +2026-07-06,2934
290 +2026-07-13,2973
291 +2026-07-20,2197
292 +2026-07-27,318
added outputs/tables/baseline_model_summary.csv +21 −0
@@ -0,0 +1,21 @@
1 +geography,property_type,n_obs,estimated,r2,median_weekly_tx
2 +quebec,all,625882,true,0.6036,1999.0
3 +quebec,unifamilial,438871,true,0.6091,1384.0
4 +quebec,condo,94792,true,0.6592,291.0
5 +quebec,plex,91971,true,0.6272,297.0
6 +montreal,all,87775,true,0.5301,281.0
7 +montreal,unifamilial,19244,true,0.6327,64.0
8 +montreal,condo,44520,true,0.5468,140.0
9 +montreal,plex,23988,true,0.1976,76.0
10 +quebec-city,all,43337,true,0.5512,122.0
11 +quebec-city,unifamilial,23606,true,0.5064,65.0
12 +quebec-city,condo,13150,true,0.5939,37.0
13 +quebec-city,plex,6572,true,0.5126,20.5
14 +laval,all,25438,true,0.5358,84.0
15 +laval,unifamilial,16762,true,0.4435,53.0
16 +laval,condo,6238,true,0.6576,21.0
17 +laval,plex,2429,true,0.5333,8.0
18 +gatineau,all,23430,true,0.5856,75.0
19 +gatineau,unifamilial,17255,true,0.5293,54.0
20 +gatineau,condo,2516,true,0.6694,6.5
21 +gatineau,plex,3657,true,0.6419,12.0
added outputs/tables/composition_shock.csv +3 −0
@@ -0,0 +1,3 @@
1 +metric,value
2 +raw_median_shift_pct,11.738
3 +hedonic_shift_pct,-0.738
added outputs/tables/composition_shock_monthly.csv +3 −0
@@ -0,0 +1,3 @@
1 +metric,value
2 +raw_median_shift_pct,12.275
3 +hedonic_shift_pct,-1.015
added outputs/tables/coverage.csv +2336 −0
@@ -0,0 +1,2336 @@
1 +municipality,geo_code,region,propertyType,median_weekly_tx,total_tx,pct_weeks_active,coverage_status
2 +Montréal,66023,Montréal,condo,140.0,44539,100.0,published
3 +Montréal,66023,Montréal,plex,76.0,24009,100.0,published
4 +Québec,23027,Capitale-Nationale,unifamilial,65.0,23614,99.0,published
5 +Montréal,66023,Montréal,unifamilial,64.0,19257,99.3,published
6 +Gatineau,81017,Outaouais,unifamilial,54.0,17261,99.0,published
7 +Laval,65005,Laval,unifamilial,53.0,16780,99.3,published
8 +Québec,23027,Capitale-Nationale,condo,37.0,13156,98.6,published
9 +Sherbrooke,43027,Estrie,unifamilial,29.0,9253,99.3,published
10 +Lévis,25213,Chaudière-Appalaches,unifamilial,26.0,9231,98.3,published
11 +Saguenay,94068,Saguenay–Lac-Saint-Jean,unifamilial,26.0,8761,96.9,published
12 +Longueuil,58227,Montérégie,unifamilial,28.0,8747,99.0,published
13 +Trois-Rivières,37067,Mauricie,unifamilial,23.0,7325,99.0,published
14 +Terrebonne,64008,Lanaudière,unifamilial,22.0,7143,100.0,conditional
15 +Québec,23027,Capitale-Nationale,plex,20.5,6577,99.0,published
16 +Laval,65005,Laval,condo,21.0,6245,99.0,published
17 +Saint-Jean-sur-Richelieu,56083,Montérégie,unifamilial,18.0,5807,99.3,conditional
18 +Drummondville,49058,Centre-du-Québec,unifamilial,16.0,5104,97.9,published
19 +Repentigny,60013,Lanaudière,unifamilial,15.0,5091,100.0,conditional
20 +Longueuil,58227,Montérégie,condo,15.0,4628,98.3,published
21 +Saint-Jérôme,75017,Laurentides,unifamilial,14.5,4503,98.3,conditional
22 +Granby,47017,Estrie,unifamilial,12.0,4024,97.3,conditional
23 +Blainville,73015,Laurentides,unifamilial,12.0,3733,97.6,conditional
24 +Mirabel,74005,Laurentides,unifamilial,12.0,3705,98.6,conditional
25 +Gatineau,81017,Outaouais,plex,12.0,3660,98.3,published
26 +Brossard,58007,Montérégie,unifamilial,12.0,3475,97.9,conditional
27 +Shawinigan,36033,Mauricie,unifamilial,11.0,3346,97.6,conditional
28 +Mascouche,64015,Lanaudière,unifamilial,10.0,3214,98.6,conditional
29 +Rimouski,10043,Bas-Saint-Laurent,unifamilial,10.0,3048,96.6,conditional
30 +Victoriaville,39062,Centre-du-Québec,unifamilial,9.0,2996,96.9,conditional
31 +Châteauguay,67050,Montérégie,unifamilial,9.0,2894,97.9,conditional
32 +Salaberry-de-Valleyfield,70052,Montérégie,unifamilial,9.0,2882,96.9,conditional
33 +Longueuil,58227,Montérégie,plex,9.0,2741,98.6,published
34 +Saint-Hyacinthe,54048,Montérégie,unifamilial,9.0,2727,96.9,conditional
35 +Saint-Eustache,72005,Laurentides,unifamilial,8.0,2576,96.9,conditional
36 +Saguenay,94068,Saguenay–Lac-Saint-Jean,plex,8.0,2576,96.2,published
37 +Boucherville,58033,Montérégie,unifamilial,8.0,2559,97.3,conditional
38 +Trois-Rivières,37067,Mauricie,plex,8.0,2535,97.6,published
39 +Gatineau,81017,Outaouais,condo,6.5,2519,96.2,published
40 +Vaudreuil-Dorion,71083,Montérégie,unifamilial,8.0,2465,99.0,conditional
41 +Laval,65005,Laval,plex,8.0,2434,97.6,published
42 +Sherbrooke,43027,Estrie,plex,7.0,2402,97.9,published
43 +Sorel-Tracy,53052,Montérégie,unifamilial,7.0,2353,97.6,conditional
44 +Saint-Lin–Laurentides,63048,Lanaudière,unifamilial,7.0,2329,98.3,conditional
45 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,unifamilial,8.0,2327,94.5,conditional
46 +Val-d'Or,89008,Abitibi-Témiscamingue,unifamilial,7.0,2194,95.9,conditional
47 +Saint-Georges,29073,Chaudière-Appalaches,unifamilial,6.0,2019,96.9,conditional
48 +Magog,45072,Estrie,unifamilial,7.0,2010,95.5,conditional
49 +Alma,93042,Saguenay–Lac-Saint-Jean,unifamilial,6.0,1975,95.2,conditional
50 +Chambly,57005,Montérégie,unifamilial,6.0,1952,95.2,conditional
51 +Sainte-Julie,59010,Montérégie,unifamilial,6.0,1943,95.5,conditional
52 +Dollard-des-Ormeaux,66142,Montréal,unifamilial,6.0,1872,96.2,conditional
53 +Brossard,58007,Montérégie,condo,5.0,1797,91.8,conditional
54 +Thetford Mines,31084,Chaudière-Appalaches,unifamilial,6.0,1773,94.5,conditional
55 +Saint-Constant,67035,Montérégie,unifamilial,5.0,1708,94.2,conditional
56 +Saint-Bruno-de-Montarville,58037,Montérégie,unifamilial,6.0,1702,94.2,conditional
57 +Beloeil,57040,Montérégie,unifamilial,5.0,1697,95.2,conditional
58 +Saint-Jérôme,75017,Laurentides,plex,5.0,1696,97.3,conditional
59 +Repentigny,60013,Lanaudière,condo,5.0,1688,96.9,conditional
60 +Sainte-Sophie,75028,Laurentides,unifamilial,6.0,1685,96.9,conditional
61 +L'Assomption,60028,Lanaudière,unifamilial,5.0,1600,94.5,conditional
62 +Saint-Colomban,75005,Laurentides,unifamilial,5.0,1568,94.5,conditional
63 +Sept-Îles,97007,Côte-Nord,unifamilial,5.0,1563,93.8,conditional
64 +Saint-Hippolyte,75045,Laurentides,unifamilial,5.0,1558,96.2,conditional
65 +Candiac,67020,Montérégie,unifamilial,5.0,1555,95.5,conditional
66 +Sainte-Adèle,77022,Laurentides,unifamilial,5.0,1534,94.8,conditional
67 +Saint-Lazare,71105,Montérégie,unifamilial,5.0,1521,94.8,conditional
68 +Boisbriand,73005,Laurentides,unifamilial,5.0,1519,96.2,conditional
69 +Lévis,25213,Chaudière-Appalaches,plex,5.0,1511,95.5,published
70 +Shawinigan,36033,Mauricie,plex,5.0,1485,96.9,conditional
71 +Sainte-Julienne,63060,Lanaudière,unifamilial,5.0,1468,96.6,conditional
72 +La Prairie,67015,Montérégie,unifamilial,5.0,1465,93.8,conditional
73 +Saint-Sauveur,77043,Laurentides,unifamilial,5.0,1461,94.8,conditional
74 +Mont-Saint-Hilaire,57035,Montérégie,unifamilial,5.0,1428,93.1,conditional
75 +Val-des-Monts,82015,Outaouais,unifamilial,4.0,1424,94.8,not_published
76 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,unifamilial,4.0,1399,93.5,not_published
77 +Lavaltrie,52007,Lanaudière,unifamilial,4.0,1393,95.5,not_published
78 +Rawdon,62037,Lanaudière,unifamilial,4.0,1366,95.5,not_published
79 +Pointe-Claire,66097,Montréal,unifamilial,4.0,1340,94.8,not_published
80 +Varennes,59020,Montérégie,unifamilial,4.0,1312,91.4,not_published
81 +Drummondville,49058,Centre-du-Québec,plex,4.0,1205,94.2,published
82 +Prévost,75040,Laurentides,unifamilial,4.0,1203,93.1,not_published
83 +Baie-Comeau,96020,Côte-Nord,unifamilial,4.0,1179,88.3,not_published
84 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,unifamilial,3.0,1162,89.7,not_published
85 +Saint-Basile-le-Grand,57020,Montérégie,unifamilial,4.0,1148,92.8,not_published
86 +Beaconsfield,66107,Montréal,unifamilial,4.0,1147,92.8,not_published
87 +Mirabel,74005,Laurentides,plex,4.0,1146,94.5,not_published
88 +Mercier,67045,Montérégie,unifamilial,4.0,1141,90.4,not_published
89 +Saint-Raymond,34128,Capitale-Nationale,unifamilial,4.0,1140,92.1,not_published
90 +Saint-Calixte,63055,Lanaudière,unifamilial,4.0,1130,93.1,not_published
91 +Bromont,46078,Estrie,unifamilial,3.0,1053,91.1,not_published
92 +Sainte-Anne-des-Plaines,73035,Laurentides,unifamilial,3.0,1049,91.8,not_published
93 +L'Ancienne-Lorette,23057,Capitale-Nationale,unifamilial,3.0,1049,89.3,not_published
94 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,unifamilial,3.0,1043,87.6,not_published
95 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,unifamilial,3.0,1039,93.1,not_published
96 +Saint-Amable,59015,Montérégie,unifamilial,3.0,1039,91.1,not_published
97 +Saint-Jean-sur-Richelieu,56083,Montérégie,plex,3.0,1029,92.8,not_published
98 +Chertsey,62047,Lanaudière,unifamilial,4.0,1026,90.7,not_published
99 +Dolbeau-Mistassini,92022,Saguenay–Lac-Saint-Jean,unifamilial,4.0,1023,88.3,not_published
100 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,unifamilial,3.0,1015,86.3,not_published
101 +Bécancour,38010,Centre-du-Québec,unifamilial,3.0,1010,91.4,not_published
102 +Deux-Montagnes,72010,Laurentides,unifamilial,3.0,1010,90.7,not_published
103 +Sainte-Catherine,67030,Montérégie,unifamilial,3.0,1004,88.7,not_published
104 +Lachute,76020,Laurentides,unifamilial,3.0,1001,90.0,not_published
105 +Rivière-du-Loup,12072,Bas-Saint-Laurent,unifamilial,3.0,990,88.0,not_published
106 +Sainte-Thérèse,73010,Laurentides,unifamilial,3.0,981,93.5,not_published
107 +Rosemère,73020,Laurentides,unifamilial,3.0,970,91.4,not_published
108 +Saint-Apollinaire,33090,Chaudière-Appalaches,unifamilial,3.0,970,88.0,not_published
109 +Mont-Tremblant,78102,Laurentides,unifamilial,3.0,968,90.4,not_published
110 +Cowansville,46080,Estrie,unifamilial,3.0,968,90.0,not_published
111 +Salaberry-de-Valleyfield,70052,Montérégie,plex,3.0,955,91.1,not_published
112 +Saint-Lambert,58012,Montérégie,unifamilial,3.0,947,89.7,not_published
113 +Granby,47017,Estrie,plex,3.0,944,86.9,not_published
114 +Terrebonne,64008,Lanaudière,condo,3.0,929,82.8,not_published
115 +Terrebonne,64008,Lanaudière,plex,3.0,920,90.7,not_published
116 +Lévis,25213,Chaudière-Appalaches,condo,3.0,919,79.4,published
117 +Pont-Rouge,34017,Capitale-Nationale,unifamilial,3.0,918,83.5,not_published
118 +Matane,08053,Bas-Saint-Laurent,unifamilial,3.0,917,87.6,not_published
119 +Kirkland,66102,Montréal,unifamilial,3.0,917,89.3,not_published
120 +Carignan,57010,Montérégie,unifamilial,3.0,912,89.3,not_published
121 +Beauharnois,70022,Montérégie,unifamilial,3.0,906,90.4,not_published
122 +Mont-Laurier,79088,Laurentides,unifamilial,3.0,904,89.7,not_published
123 +Contrecoeur,59035,Montérégie,unifamilial,3.0,884,87.6,not_published
124 +Sainte-Agathe-des-Monts,78032,Laurentides,unifamilial,3.0,882,92.1,not_published
125 +Cantley,82020,Outaouais,unifamilial,3.0,878,88.0,not_published
126 +Saint-Adolphe-d'Howard,77065,Laurentides,unifamilial,3.0,865,91.4,not_published
127 +Saint-Zotique,71025,Montérégie,unifamilial,3.0,855,87.6,not_published
128 +Pincourt,71070,Montérégie,unifamilial,3.0,852,87.6,not_published
129 +Saint-Donat,62060,Lanaudière,unifamilial,3.0,846,89.0,not_published
130 +Marieville,55048,Montérégie,unifamilial,3.0,834,86.9,not_published
131 +Joliette,61025,Lanaudière,unifamilial,3.0,827,88.3,not_published
132 +Chelsea,82025,Outaouais,unifamilial,3.0,809,82.1,not_published
133 +Sainte-Marie,26030,Chaudière-Appalaches,unifamilial,3.0,808,82.8,not_published
134 +La Pêche,82035,Outaouais,unifamilial,3.0,807,86.6,not_published
135 +Saint-Hyacinthe,54048,Montérégie,plex,3.0,800,86.9,not_published
136 +Lorraine,73025,Laurentides,unifamilial,3.0,796,85.6,not_published
137 +Les Îles-de-la-Madeleine,01023,Gaspésie–Îles-de-la-Madeleine,unifamilial,3.0,778,88.0,not_published
138 +Westmount,66032,Montréal,unifamilial,3.0,765,86.3,not_published
139 +Saint-Jérôme,75017,Laurentides,condo,2.0,760,75.9,not_published
140 +Brownsburg-Chatham,76043,Laurentides,unifamilial,2.0,756,89.3,not_published
141 +Saint-Félicien,91042,Saguenay–Lac-Saint-Jean,unifamilial,3.0,755,86.3,not_published
142 +Lac-Beauport,22040,Capitale-Nationale,unifamilial,2.0,751,85.6,not_published
143 +Saint-Philippe,67010,Montérégie,unifamilial,3.0,748,84.5,not_published
144 +Otterburn Park,57030,Montérégie,unifamilial,2.0,745,86.9,not_published
145 +Gaspé,03005,Gaspésie–Îles-de-la-Madeleine,unifamilial,3.0,742,87.3,not_published
146 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,unifamilial,3.0,741,85.9,not_published
147 +Farnham,46112,Estrie,unifamilial,3.0,719,83.5,not_published
148 +La Tuque,90012,Mauricie,unifamilial,3.0,714,84.5,not_published
149 +Mascouche,64015,Lanaudière,condo,2.0,713,78.0,not_published
150 +Mont-Royal,66072,Montréal,unifamilial,2.0,711,86.9,not_published
151 +Orford,45115,Estrie,unifamilial,3.0,703,84.5,not_published
152 +Notre-Dame-des-Prairies,61030,Lanaudière,unifamilial,2.0,691,82.8,not_published
153 +Côte-Saint-Luc,66058,Montréal,condo,2.5,681,81.8,not_published
154 +Sorel-Tracy,53052,Montérégie,plex,2.0,670,87.3,not_published
155 +Côte-Saint-Luc,66058,Montréal,unifamilial,2.0,668,83.2,not_published
156 +Chibougamau,99025,Nord-du-Québec,unifamilial,2.0,662,80.4,not_published
157 +Dorval,66087,Montréal,unifamilial,2.0,660,80.8,not_published
158 +L'Épiphanie,60037,Lanaudière,unifamilial,2.0,651,82.5,not_published
159 +Saint-Charles-Borromée,61035,Lanaudière,unifamilial,2.0,645,84.5,not_published
160 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,unifamilial,2.0,643,84.9,not_published
161 +Boischatel,21045,Capitale-Nationale,unifamilial,2.0,638,79.7,not_published
162 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,plex,2.0,633,83.5,not_published
163 +Bois-des-Filion,73030,Laurentides,unifamilial,2.0,631,83.2,not_published
164 +Roberval,91025,Saguenay–Lac-Saint-Jean,unifamilial,2.0,625,82.1,not_published
165 +Shefford,47035,Estrie,unifamilial,2.0,624,84.5,not_published
166 +Lac-Brome,46075,Estrie,unifamilial,2.0,619,80.8,not_published
167 +Morin-Heights,77050,Laurentides,unifamilial,2.0,612,81.1,not_published
168 +Rigaud,71133,Montérégie,unifamilial,2.0,609,82.8,not_published
169 +Montmagny,18050,Chaudière-Appalaches,unifamilial,2.0,605,80.8,not_published
170 +La Malbaie,15013,Capitale-Nationale,unifamilial,2.0,596,81.8,not_published
171 +Saint-Jean-de-Matha,62015,Lanaudière,unifamilial,2.0,594,75.6,not_published
172 +Donnacona,34025,Capitale-Nationale,unifamilial,2.0,588,77.3,not_published
173 +Shannon,22020,Capitale-Nationale,unifamilial,2.0,587,69.8,not_published
174 +Acton Vale,48028,Montérégie,unifamilial,2.0,586,78.4,not_published
175 +Blainville,73015,Laurentides,plex,2.0,576,80.4,not_published
176 +Saint-Lambert,58012,Montérégie,condo,2.0,574,70.1,not_published
177 +Saint-Honoré,94240,Saguenay–Lac-Saint-Jean,unifamilial,2.0,570,79.0,not_published
178 +Boucherville,58033,Montérégie,condo,2.0,570,65.6,not_published
179 +Pointe-Calumet,72020,Laurentides,unifamilial,2.0,568,81.1,not_published
180 +Val-David,78010,Laurentides,unifamilial,2.0,566,78.4,not_published
181 +Hébertville,93022,Saguenay–Lac-Saint-Jean,unifamilial,2.0,561,75.9,not_published
182 +Saint-Paul,61005,Lanaudière,unifamilial,2.0,555,78.7,not_published
183 +Saint-Alphonse-Rodriguez,62025,Lanaudière,unifamilial,2.0,554,85.9,not_published
184 +Alma,93042,Saguenay–Lac-Saint-Jean,plex,2.0,554,76.3,not_published
185 +Val-des-Sources,40043,Estrie,unifamilial,2.0,547,76.6,not_published
186 +Coaticook,44037,Estrie,unifamilial,2.0,546,77.7,not_published
187 +Waterloo,47025,Estrie,unifamilial,2.0,544,75.3,not_published
188 +L'Île-Perrot,71060,Montérégie,unifamilial,2.0,542,75.6,not_published
189 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,condo,2.0,538,72.5,not_published
190 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,unifamilial,2.0,538,78.7,not_published
191 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,unifamilial,2.0,537,74.9,not_published
192 +Saint-Félix-de-Valois,62007,Lanaudière,unifamilial,2.0,532,76.6,not_published
193 +Victoriaville,39062,Centre-du-Québec,plex,2.0,526,75.9,not_published
194 +Hudson,71100,Montérégie,unifamilial,2.0,520,75.9,not_published
195 +L'Ange-Gardien,82005,Outaouais,unifamilial,2.0,515,75.3,not_published
196 +Pointe-Claire,66097,Montréal,condo,2.0,508,72.5,not_published
197 +Roxton Pond,47047,Estrie,unifamilial,2.0,504,74.6,not_published
198 +Coteau-du-Lac,71040,Montérégie,unifamilial,2.0,503,74.9,not_published
199 +Sherbrooke,43027,Estrie,condo,2.0,498,76.3,published
200 +Delson,67025,Montérégie,unifamilial,2.0,494,72.2,not_published
201 +Les Cèdres,71050,Montérégie,unifamilial,2.0,494,73.5,not_published
202 +Magog,45072,Estrie,condo,2.0,493,76.6,not_published
203 +Lanoraie,52017,Lanaudière,unifamilial,2.0,493,75.9,not_published
204 +Chambly,57005,Montérégie,condo,2.0,491,67.0,not_published
205 +Sainte-Thérèse,73010,Laurentides,plex,2.0,490,74.9,not_published
206 +Saint-Rémi,68055,Montérégie,unifamilial,2.0,490,75.9,not_published
207 +Saint-Jean-sur-Richelieu,56083,Montérégie,condo,2.0,488,67.7,not_published
208 +Saint-Côme,62065,Lanaudière,unifamilial,2.0,488,75.6,not_published
209 +Mascouche,64015,Lanaudière,plex,2.0,486,75.3,not_published
210 +Saint-Pie,54008,Montérégie,unifamilial,2.0,484,72.9,not_published
211 +Saint-Lin–Laurentides,63048,Lanaudière,plex,2.0,483,75.9,not_published
212 +Sainte-Anne-des-Lacs,77035,Laurentides,unifamilial,2.0,481,73.9,not_published
213 +Notre-Dame-du-Mont-Carmel,37235,Mauricie,unifamilial,2.0,480,74.6,not_published
214 +Mont-Tremblant,78102,Laurentides,condo,2.0,478,72.9,not_published
215 +Thetford Mines,31084,Chaudière-Appalaches,plex,2.0,476,74.9,not_published
216 +Saint-Damien,62075,Lanaudière,unifamilial,2.0,474,76.6,not_published
217 +Rivière-Rouge,79037,Laurentides,unifamilial,2.0,472,73.5,not_published
218 +Nicolet,50072,Centre-du-Québec,unifamilial,2.0,472,72.9,not_published
219 +Mont-Blanc,78047,Laurentides,unifamilial,2.0,468,77.3,not_published
220 +Sainte-Anne-des-Monts,04037,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,464,73.5,not_published
221 +Les Coteaux,71033,Montérégie,unifamilial,2.0,462,73.9,not_published
222 +Piedmont,77030,Laurentides,unifamilial,2.0,455,73.9,not_published
223 +Saint-Denis-de-Brompton,42025,Estrie,unifamilial,2.0,454,73.2,not_published
224 +Pontiac,82030,Outaouais,unifamilial,2.0,446,75.6,not_published
225 +Saint-Alexis-des-Monts,51065,Mauricie,unifamilial,2.0,445,75.9,not_published
226 +Granby,47017,Estrie,condo,2.0,442,64.3,not_published
227 +Dollard-des-Ormeaux,66142,Montréal,condo,2.0,439,67.7,not_published
228 +Val-d'Or,89008,Abitibi-Témiscamingue,plex,2.0,431,75.6,not_published
229 +Saint-Agapit,33045,Chaudière-Appalaches,unifamilial,2.0,431,71.1,not_published
230 +Sainte-Sophie,75028,Laurentides,plex,2.0,430,72.2,not_published
231 +Dorval,66087,Montréal,condo,2.0,424,70.4,not_published
232 +La Sarre,87090,Abitibi-Témiscamingue,unifamilial,2.0,424,70.8,not_published
233 +Sutton,46058,Estrie,unifamilial,2.0,415,74.9,not_published
234 +Mont-Joli,09077,Bas-Saint-Laurent,unifamilial,2.0,415,68.0,not_published
235 +Repentigny,60013,Lanaudière,plex,2.0,412,72.9,not_published
236 +Beaupré,21025,Capitale-Nationale,unifamilial,2.0,408,67.7,not_published
237 +Saint-Henri,19068,Chaudière-Appalaches,unifamilial,2.0,406,66.3,not_published
238 +Joliette,61025,Lanaudière,plex,2.0,404,72.5,not_published
239 +Port-Cartier,97022,Côte-Nord,unifamilial,2.0,400,67.0,not_published
240 +Verchères,59025,Montérégie,unifamilial,2.0,398,67.7,not_published
241 +Princeville,32033,Centre-du-Québec,unifamilial,2.0,398,67.7,not_published
242 +Château-Richer,21035,Capitale-Nationale,unifamilial,2.0,396,63.2,not_published
243 +Rimouski,10043,Bas-Saint-Laurent,plex,2.0,396,64.9,not_published
244 +Saint-Césaire,55023,Montérégie,unifamilial,2.0,391,62.9,not_published
245 +Louiseville,51015,Mauricie,unifamilial,2.0,391,67.4,not_published
246 +Mandeville,52095,Lanaudière,unifamilial,2.0,381,67.4,not_published
247 +Sainte-Thérèse,73010,Laurentides,condo,2.0,380,60.1,not_published
248 +Gore,76025,Laurentides,unifamilial,2.0,378,71.8,not_published
249 +Eastman,45093,Estrie,unifamilial,2.0,378,70.1,not_published
250 +Trois-Rivières,37067,Mauricie,condo,2.0,376,56.4,published
251 +Saint-Roch-de-Richelieu,53040,Montérégie,unifamilial,2.0,375,64.6,not_published
252 +Saint-Joseph-du-Lac,72025,Laurentides,unifamilial,2.0,374,67.0,not_published
253 +Magog,45072,Estrie,plex,2.0,372,67.4,not_published
254 +Val-Morin,78005,Laurentides,unifamilial,2.0,371,70.4,not_published
255 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,unifamilial,2.0,371,67.4,not_published
256 +Lac-Supérieur,78095,Laurentides,unifamilial,2.0,370,68.7,not_published
257 +Notre-Dame-du-Laus,79005,Laurentides,unifamilial,2.0,368,66.0,not_published
258 +Lac-Etchemin,28053,Chaudière-Appalaches,unifamilial,2.0,365,64.3,not_published
259 +Saint-Michel-des-Saints,62085,Lanaudière,unifamilial,2.0,365,67.7,not_published
260 +Saint-Eustache,72005,Laurentides,condo,2.0,365,58.8,not_published
261 +Baie-Saint-Paul,16013,Capitale-Nationale,unifamilial,2.0,364,64.9,not_published
262 +Saint-Roch-de-l'Achigan,63035,Lanaudière,unifamilial,2.0,362,67.0,not_published
263 +Sainte-Adèle,77022,Laurentides,plex,2.0,361,65.3,not_published
264 +Chandler,02028,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,357,69.4,not_published
265 +Saint-Anicet,69070,Montérégie,unifamilial,2.0,354,64.6,not_published
266 +Grenville-sur-la-Rouge,76052,Laurentides,unifamilial,1.0,351,68.7,not_published
267 +Wentworth-Nord,77060,Laurentides,unifamilial,1.0,347,66.7,not_published
268 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,unifamilial,2.0,344,60.8,not_published
269 +Neuville,34007,Capitale-Nationale,unifamilial,1.0,343,62.9,not_published
270 +Sainte-Agathe-des-Monts,78032,Laurentides,plex,1.0,340,63.2,not_published
271 +Austin,45085,Estrie,unifamilial,1.0,339,66.3,not_published
272 +Thurso,80050,Outaouais,unifamilial,1.0,336,63.2,not_published
273 +Saint-Jacques,63013,Lanaudière,unifamilial,1.5,335,61.2,not_published
274 +Labelle,78120,Laurentides,unifamilial,1.0,334,61.9,not_published
275 +Lac-Mégantic,30030,Estrie,unifamilial,1.0,333,60.8,not_published
276 +Saint-Cyrille-de-Wendover,49070,Centre-du-Québec,unifamilial,1.0,329,59.8,not_published
277 +Lachute,76020,Laurentides,plex,1.0,328,65.6,not_published
278 +Saint-André-Avellin,80027,Outaouais,unifamilial,1.0,327,63.6,not_published
279 +Venise-en-Québec,56005,Montérégie,unifamilial,2.0,327,61.9,not_published
280 +Gracefield,83032,Outaouais,unifamilial,1.0,326,60.5,not_published
281 +La Pocatière,14082,Bas-Saint-Laurent,unifamilial,1.0,325,61.5,not_published
282 +Napierville,68030,Montérégie,unifamilial,1.0,325,59.5,not_published
283 +Saint-Boniface,51085,Mauricie,unifamilial,1.0,325,63.9,not_published
284 +Saint-Gabriel-de-Brandon,52085,Lanaudière,unifamilial,1.0,321,61.9,not_published
285 +Nominingue,79030,Laurentides,unifamilial,1.0,321,63.6,not_published
286 +Amqui,07047,Bas-Saint-Laurent,unifamilial,1.5,321,57.7,not_published
287 +Saguenay,94068,Saguenay–Lac-Saint-Jean,condo,2.0,321,54.3,published
288 +Saint-Eustache,72005,Laurentides,plex,1.0,320,63.9,not_published
289 +Oka,72032,Laurentides,unifamilial,1.0,320,62.9,not_published
290 +Danville,40047,Estrie,unifamilial,1.0,319,63.6,not_published
291 +Sainte-Julie,59010,Montérégie,condo,1.0,318,61.5,not_published
292 +Saint-Ambroise,94255,Saguenay–Lac-Saint-Jean,unifamilial,1.0,318,59.8,not_published
293 +Potton,45030,Estrie,unifamilial,1.0,317,63.9,not_published
294 +Weedon,41098,Estrie,unifamilial,1.0,314,64.6,not_published
295 +Candiac,67020,Montérégie,condo,2.0,314,54.0,not_published
296 +Beauceville,27028,Chaudière-Appalaches,unifamilial,1.0,314,59.5,not_published
297 +Amherst,78070,Laurentides,unifamilial,1.0,313,62.5,not_published
298 +Sainte-Béatrix,62020,Lanaudière,unifamilial,1.0,312,66.0,not_published
299 +Cap-Santé,34030,Capitale-Nationale,unifamilial,1.0,312,59.8,not_published
300 +Saint-Mathias-sur-Richelieu,55065,Montérégie,unifamilial,1.0,311,63.2,not_published
301 +Saint-Gilles,33035,Chaudière-Appalaches,unifamilial,2.0,310,61.5,not_published
302 +Portneuf,34048,Capitale-Nationale,unifamilial,2.0,310,57.0,not_published
303 +Saint-Étienne-des-Grès,51090,Mauricie,unifamilial,1.0,309,60.1,not_published
304 +Saint-Tite,35027,Mauricie,unifamilial,2.0,308,61.5,not_published
305 +Windsor,42088,Estrie,unifamilial,1.0,307,57.0,not_published
306 +Saint-Bruno-de-Montarville,58037,Montérégie,condo,1.0,306,62.9,not_published
307 +Sainte-Martine,70012,Montérégie,unifamilial,1.0,302,61.9,not_published
308 +Métabetchouan–Lac-à-la-Croix,93012,Saguenay–Lac-Saint-Jean,unifamilial,1.0,301,60.5,not_published
309 +Saint-Anselme,19062,Chaudière-Appalaches,unifamilial,1.0,299,55.0,not_published
310 +Plessisville,32043,Centre-du-Québec,unifamilial,2.0,299,51.5,not_published
311 +Cookshire-Eaton,41038,Estrie,unifamilial,1.0,298,60.8,not_published
312 +Saint-Paul-de-l'Île-aux-Noix,56035,Montérégie,unifamilial,1.0,298,58.8,not_published
313 +Maniwaki,83065,Outaouais,unifamilial,1.0,291,57.0,not_published
314 +Richelieu,55057,Montérégie,unifamilial,1.0,289,60.1,not_published
315 +Saint-Germain-de-Grantham,49048,Centre-du-Québec,unifamilial,1.0,288,58.8,not_published
316 +East Angus,41060,Estrie,unifamilial,1.0,288,61.5,not_published
317 +Lac-Simon,80095,Outaouais,unifamilial,1.0,284,59.1,not_published
318 +Saint-Joseph-de-Beauce,27043,Chaudière-Appalaches,unifamilial,1.0,282,56.7,not_published
319 +Beaumont,19105,Chaudière-Appalaches,unifamilial,1.0,280,55.3,not_published
320 +Fossambault-sur-le-Lac,22010,Capitale-Nationale,unifamilial,1.0,280,55.3,not_published
321 +Dunham,46050,Estrie,unifamilial,1.0,279,60.1,not_published
322 +Saint-Prosper,28020,Chaudière-Appalaches,unifamilial,1.0,279,58.1,not_published
323 +Saint-Georges,29073,Chaudière-Appalaches,plex,1.0,279,56.7,not_published
324 +McMasterville,57025,Montérégie,unifamilial,1.0,278,57.0,not_published
325 +Saint-Antonin,12015,Bas-Saint-Laurent,unifamilial,1.0,275,54.6,not_published
326 +Saint-André-d'Argenteuil,76008,Laurentides,unifamilial,1.0,275,55.3,not_published
327 +Lac-des-Écorces,79078,Laurentides,unifamilial,1.0,273,56.7,not_published
328 +Sainte-Émélie-de-l'Énergie,62070,Lanaudière,unifamilial,1.0,273,54.3,not_published
329 +Adstock,31056,Chaudière-Appalaches,unifamilial,1.0,272,59.5,not_published
330 +Baie-Comeau,96020,Côte-Nord,plex,1.0,272,51.9,not_published
331 +Châteauguay,67050,Montérégie,plex,1.0,271,55.3,not_published
332 +Sainte-Mélanie,61050,Lanaudière,unifamilial,1.0,270,58.4,not_published
333 +La Minerve,78130,Laurentides,unifamilial,1.0,267,59.5,not_published
334 +Notre-Dame-de-la-Merci,62055,Lanaudière,unifamilial,1.0,267,60.1,not_published
335 +Warwick,39077,Centre-du-Québec,unifamilial,1.0,265,57.0,not_published
336 +Beloeil,57040,Montérégie,condo,1.0,264,50.9,not_published
337 +Sainte-Claire,19055,Chaudière-Appalaches,unifamilial,1.0,264,52.9,not_published
338 +Saint-Sauveur,77043,Laurentides,plex,1.0,263,56.4,not_published
339 +Huntingdon,69055,Montérégie,unifamilial,1.0,262,53.3,not_published
340 +Rivière-Beaudette,71005,Montérégie,unifamilial,1.0,260,54.6,not_published
341 +Saint-Ambroise-de-Kildare,61040,Lanaudière,unifamilial,1.0,260,55.7,not_published
342 +Baie-D'Urfé,66112,Montréal,unifamilial,1.0,259,57.0,not_published
343 +Saint-Maurice,37230,Mauricie,unifamilial,1.0,256,52.2,not_published
344 +Mont-Royal,66072,Montréal,condo,1.0,256,51.2,not_published
345 +La Tuque,90012,Mauricie,plex,1.0,255,54.0,not_published
346 +Hampstead,66062,Montréal,unifamilial,1.0,254,56.7,not_published
347 +Rivière-du-Loup,12072,Bas-Saint-Laurent,plex,1.0,254,54.6,not_published
348 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,condo,1.0,252,49.8,not_published
349 +Mont-Laurier,79088,Laurentides,plex,1.0,251,56.0,not_published
350 +Saint-Isidore,26063,Chaudière-Appalaches,unifamilial,1.0,250,51.9,not_published
351 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,247,51.9,not_published
352 +Sept-Îles,97007,Côte-Nord,plex,1.0,247,56.4,not_published
353 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,unifamilial,1.0,246,51.2,not_published
354 +Crabtree,61013,Lanaudière,unifamilial,1.0,246,54.3,not_published
355 +Saint-Alphonse-de-Granby,47010,Estrie,unifamilial,1.0,245,51.5,not_published
356 +Ormstown,69037,Montérégie,unifamilial,1.0,244,53.3,not_published
357 +Berthierville,52035,Lanaudière,unifamilial,1.0,244,52.6,not_published
358 +Saint-Gabriel-de-Valcartier,22025,Capitale-Nationale,unifamilial,1.0,244,48.8,not_published
359 +Saint-Élie-de-Caxton,51075,Mauricie,unifamilial,1.0,244,54.0,not_published
360 +Saint-Basile,34038,Capitale-Nationale,unifamilial,1.0,243,51.2,not_published
361 +Sainte-Luce,09092,Bas-Saint-Laurent,unifamilial,1.0,243,54.3,not_published
362 +Malartic,89015,Abitibi-Témiscamingue,unifamilial,1.0,242,54.6,not_published
363 +L'Islet,17078,Chaudière-Appalaches,unifamilial,1.0,241,50.2,not_published
364 +Saint-Gabriel,52080,Lanaudière,unifamilial,1.0,241,54.0,not_published
365 +Sainte-Catherine-de-Hatley,45060,Estrie,unifamilial,1.0,240,51.5,not_published
366 +Sainte-Croix,33102,Chaudière-Appalaches,unifamilial,1.0,239,52.6,not_published
367 +Blainville,73015,Laurentides,condo,1.0,239,48.8,not_published
368 +Notre-Dame-des-Bois,30010,Estrie,unifamilial,1.0,238,54.0,not_published
369 +Lacolle,56023,Montérégie,unifamilial,1.0,236,52.2,not_published
370 +Sainte-Lucie-des-Laurentides,78020,Laurentides,unifamilial,1.0,235,53.6,not_published
371 +Papineauville,80037,Outaouais,unifamilial,1.0,234,53.3,not_published
372 +Cowansville,46080,Estrie,plex,1.0,234,49.8,not_published
373 +Beauharnois,70022,Montérégie,plex,1.0,231,53.3,not_published
374 +Saint-Denis-sur-Richelieu,57068,Montérégie,unifamilial,1.0,230,49.5,not_published
375 +Westmount,66032,Montréal,condo,1.0,230,50.5,not_published
376 +Léry,67055,Montérégie,unifamilial,1.0,230,49.8,not_published
377 +Saint-Liboire,54072,Montérégie,unifamilial,1.0,230,51.5,not_published
378 +Mont-Tremblant,78102,Laurentides,plex,1.0,229,53.6,not_published
379 +Vaudreuil-Dorion,71083,Montérégie,condo,1.0,229,41.9,not_published
380 +Saint-Christophe-d'Arthabaska,39060,Centre-du-Québec,unifamilial,1.0,228,53.6,not_published
381 +Ascot Corner,41055,Estrie,unifamilial,1.0,228,50.5,not_published
382 +L'Assomption,60028,Lanaudière,plex,1.0,226,53.6,not_published
383 +New Richmond,05070,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,226,51.2,not_published
384 +Mont-Saint-Grégoire,56097,Montérégie,unifamilial,1.0,224,51.2,not_published
385 +Montpellier,80090,Outaouais,unifamilial,1.0,223,50.5,not_published
386 +Saint-Zénon,62080,Lanaudière,unifamilial,1.0,223,48.8,not_published
387 +Trois-Pistoles,11040,Bas-Saint-Laurent,unifamilial,1.0,222,50.5,not_published
388 +Saint-Jean-Port-Joli,17070,Chaudière-Appalaches,unifamilial,1.0,222,48.8,not_published
389 +Saint-Raphaël,19082,Chaudière-Appalaches,unifamilial,1.0,222,50.9,not_published
390 +Saint-Mathieu-de-Beloeil,57045,Montérégie,unifamilial,1.0,221,46.4,not_published
391 +Sainte-Marie-Madeleine,54030,Montérégie,unifamilial,1.0,220,47.8,not_published
392 +Sainte-Clotilde,68020,Montérégie,unifamilial,1.0,220,49.1,not_published
393 +Ferme-Neuve,79097,Laurentides,unifamilial,1.0,219,46.7,not_published
394 +Sainte-Anne-de-Sabrevois,56060,Montérégie,unifamilial,1.0,218,45.7,not_published
395 +Saint-Colomban,75005,Laurentides,plex,1.0,217,46.7,not_published
396 +Sainte-Anne-de-Bellevue,66117,Montréal,unifamilial,1.0,216,48.1,not_published
397 +Saint-Alexandre,56055,Montérégie,unifamilial,1.0,216,47.8,not_published
398 +Daveluyville,39152,Centre-du-Québec,unifamilial,1.0,215,50.9,not_published
399 +Sainte-Thècle,35050,Mauricie,unifamilial,1.0,215,47.8,not_published
400 +Saint-Mathieu-du-Parc,51070,Mauricie,unifamilial,1.0,214,51.9,not_published
401 +La Conception,78115,Laurentides,unifamilial,1.0,214,51.2,not_published
402 +Saint-Amable,59015,Montérégie,condo,1.0,213,44.7,not_published
403 +Saint-Louis-de-Gonzague,70035,Montérégie,unifamilial,1.0,212,47.8,not_published
404 +Notre-Dame-de-Lourdes,61045,Lanaudière,unifamilial,1.0,211,46.4,not_published
405 +Témiscaming,85005,Abitibi-Témiscamingue,unifamilial,1.0,211,50.2,not_published
406 +Sainte-Marcelline-de-Kildare,62030,Lanaudière,unifamilial,1.0,211,51.2,not_published
407 +Saint-Hippolyte,75045,Laurentides,plex,1.0,210,49.1,not_published
408 +Hinchinbrooke,69045,Montérégie,unifamilial,1.0,209,50.5,not_published
409 +Saint-Marc-des-Carrières,34065,Capitale-Nationale,unifamilial,1.0,208,43.6,not_published
410 +Brigham,46090,Estrie,unifamilial,1.0,208,48.8,not_published
411 +Saint-Dominique,54060,Montérégie,unifamilial,1.0,208,48.5,not_published
412 +Rougemont,55037,Montérégie,unifamilial,1.0,207,48.8,not_published
413 +Amos,88057,Abitibi-Témiscamingue,unifamilial,2.0,206,28.5,not_published
414 +Clermont,15035,Capitale-Nationale,unifamilial,1.0,205,45.4,not_published
415 +Farnham,46112,Estrie,plex,1.0,205,47.4,not_published
416 +Montréal-Ouest,66047,Montréal,unifamilial,1.0,205,44.7,not_published
417 +Saint-Barthélemy,52055,Lanaudière,unifamilial,1.0,205,50.5,not_published
418 +Entrelacs,62053,Lanaudière,unifamilial,1.0,204,49.8,not_published
419 +Disraeli,31015,Chaudière-Appalaches,unifamilial,1.0,203,48.8,not_published
420 +Saint-Stanislas-de-Kostka,70040,Montérégie,unifamilial,1.0,203,46.7,not_published
421 +Saint-Polycarpe,71020,Montérégie,unifamilial,1.0,203,48.8,not_published
422 +Val-des-Bois,80140,Outaouais,unifamilial,1.0,202,44.0,not_published
423 +Stanstead,45008,Estrie,unifamilial,1.0,202,50.9,not_published
424 +Saint-Côme–Linière,29057,Chaudière-Appalaches,unifamilial,1.0,201,43.6,not_published
425 +Scott,26048,Chaudière-Appalaches,unifamilial,1.0,200,47.1,not_published
426 +Ange-Gardien,55008,Montérégie,unifamilial,1.0,199,46.4,not_published
427 +Saint-Lucien,49030,Centre-du-Québec,unifamilial,1.0,199,47.8,not_published
428 +Mille-Isles,76030,Laurentides,unifamilial,1.0,198,48.5,not_published
429 +Lac-aux-Sables,35010,Mauricie,unifamilial,1.0,198,48.1,not_published
430 +Saint-Michel,68050,Montérégie,unifamilial,1.0,197,45.7,not_published
431 +Cap-Saint-Ignace,18045,Chaudière-Appalaches,unifamilial,1.0,197,46.0,not_published
432 +Saint-Chrysostome,69017,Montérégie,unifamilial,1.0,197,47.8,not_published
433 +Saint-Charles-de-Bellechasse,19097,Chaudière-Appalaches,unifamilial,1.0,196,43.0,not_published
434 +Saint-Joseph-de-Coleraine,31045,Chaudière-Appalaches,unifamilial,1.0,196,44.3,not_published
435 +Normandin,92040,Saguenay–Lac-Saint-Jean,unifamilial,1.0,196,47.4,not_published
436 +Harrington,76065,Laurentides,unifamilial,1.0,195,47.4,not_published
437 +Saint-Blaise-sur-Richelieu,56065,Montérégie,unifamilial,1.0,195,44.7,not_published
438 +L'Ascension-de-Notre-Seigneur,93065,Saguenay–Lac-Saint-Jean,unifamilial,1.0,195,43.3,not_published
439 +Dolbeau-Mistassini,92022,Saguenay–Lac-Saint-Jean,plex,1.0,195,43.3,not_published
440 +Stoke,42005,Estrie,unifamilial,1.0,195,47.1,not_published
441 +Chambly,57005,Montérégie,plex,1.0,195,44.7,not_published
442 +Berthier-sur-Mer,18065,Chaudière-Appalaches,unifamilial,1.0,195,44.3,not_published
443 +Ripon,80078,Outaouais,unifamilial,1.0,194,46.0,not_published
444 +Stratford,30110,Estrie,unifamilial,1.0,193,45.0,not_published
445 +Sainte-Anne-de-Sorel,53065,Montérégie,unifamilial,1.0,192,46.7,not_published
446 +Sainte-Barbe,69065,Montérégie,unifamilial,1.0,192,43.0,not_published
447 +Saint-François-du-Lac,50128,Centre-du-Québec,unifamilial,1.0,192,47.4,not_published
448 +Charlemagne,60005,Lanaudière,unifamilial,1.0,191,43.6,not_published
449 +Saint-Thomas,61027,Lanaudière,unifamilial,1.0,190,45.4,not_published
450 +Richmond,42098,Estrie,unifamilial,1.0,188,47.4,not_published
451 +Saint-Sulpice,60020,Lanaudière,unifamilial,1.0,188,45.0,not_published
452 +Deschambault-Grondines,34058,Capitale-Nationale,unifamilial,1.0,188,44.7,not_published
453 +Saint-Elzéar,26022,Chaudière-Appalaches,unifamilial,1.0,188,44.7,not_published
454 +Pohénégamook,13095,Bas-Saint-Laurent,unifamilial,1.0,188,44.7,not_published
455 +Bolton-Est,45095,Estrie,unifamilial,1.0,187,46.0,not_published
456 +Matane,08053,Bas-Saint-Laurent,plex,1.0,187,41.9,not_published
457 +Lebel-sur-Quévillon,99005,Nord-du-Québec,unifamilial,1.0,186,44.7,not_published
458 +Bedford,46035,Estrie,unifamilial,1.0,186,43.6,not_published
459 +Macamic,87058,Abitibi-Témiscamingue,unifamilial,1.0,186,45.4,not_published
460 +Brossard,58007,Montérégie,plex,1.0,184,43.3,not_published
461 +Waterville,44080,Estrie,unifamilial,1.0,184,41.2,not_published
462 +Havre-Saint-Pierre,98040,Côte-Nord,unifamilial,1.0,184,46.4,not_published
463 +Boischatel,21045,Capitale-Nationale,condo,1.0,182,39.5,not_published
464 +Saint-Anaclet-de-Lessard,10030,Bas-Saint-Laurent,unifamilial,1.0,182,44.3,not_published
465 +Pierreville,50113,Centre-du-Québec,unifamilial,1.0,182,44.7,not_published
466 +Saint-Tite-des-Caps,21005,Capitale-Nationale,unifamilial,1.0,181,42.6,not_published
467 +Saint-Prime,91035,Saguenay–Lac-Saint-Jean,unifamilial,1.0,181,44.3,not_published
468 +Saint-Paul-d'Abbotsford,55015,Montérégie,unifamilial,1.0,180,43.6,not_published
469 +Senneterre,89040,Abitibi-Témiscamingue,unifamilial,1.0,179,40.5,not_published
470 +Saint-Bernard,26055,Chaudière-Appalaches,unifamilial,1.0,178,40.9,not_published
471 +Saint-Jean-Baptiste,57033,Montérégie,unifamilial,1.0,177,44.0,not_published
472 +Compton,44071,Estrie,unifamilial,1.0,176,42.3,not_published
473 +Saint-Antoine-de-Tilly,33095,Chaudière-Appalaches,unifamilial,1.0,176,41.2,not_published
474 +Larouche,94265,Saguenay–Lac-Saint-Jean,unifamilial,1.0,176,40.9,not_published
475 +Percé,02005,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,176,44.0,not_published
476 +Maskinongé,51008,Mauricie,unifamilial,1.0,176,44.7,not_published
477 +Saint-Pascal,14018,Bas-Saint-Laurent,unifamilial,1.0,175,44.7,not_published
478 +Sainte-Victoire-de-Sorel,53025,Montérégie,unifamilial,1.0,175,41.6,not_published
479 +Beloeil,57040,Montérégie,plex,1.0,175,39.2,not_published
480 +Saint-Isidore,67040,Montérégie,unifamilial,1.0,173,43.3,not_published
481 +Mansfield-et-Pontefract,84065,Outaouais,unifamilial,1.0,173,40.9,not_published
482 +Chute-Saint-Philippe,79065,Laurentides,unifamilial,1.0,173,44.7,not_published
483 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,unifamilial,1.0,172,42.6,not_published
484 +Messines,83060,Outaouais,unifamilial,1.0,172,44.7,not_published
485 +Noyan,56015,Montérégie,unifamilial,1.0,172,40.9,not_published
486 +Wickham,49040,Centre-du-Québec,unifamilial,1.0,172,42.3,not_published
487 +Laurier-Station,33060,Chaudière-Appalaches,unifamilial,1.0,171,41.2,not_published
488 +Saint-Gédéon-de-Beauce,29013,Chaudière-Appalaches,unifamilial,1.0,171,41.6,not_published
489 +Maria,06005,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,170,41.9,not_published
490 +Rimouski,10043,Bas-Saint-Laurent,condo,1.0,169,41.2,not_published
491 +L'Isle-aux-Allumettes,84082,Outaouais,unifamilial,1.0,169,43.0,not_published
492 +Yamachiche,51020,Mauricie,unifamilial,1.0,168,40.2,not_published
493 +East Broughton,31122,Chaudière-Appalaches,unifamilial,1.0,168,40.5,not_published
494 +Ville-Marie,85025,Abitibi-Témiscamingue,unifamilial,1.0,168,41.9,not_published
495 +Saint-Antoine-sur-Richelieu,57075,Montérégie,unifamilial,1.0,168,40.2,not_published
496 +Val-des-Sources,40043,Estrie,plex,1.0,167,43.0,not_published
497 +Clarenceville,56010,Montérégie,unifamilial,1.0,167,41.6,not_published
498 +Piedmont,77030,Laurentides,condo,1.0,167,37.5,not_published
499 +Hemmingford,68015,Montérégie,unifamilial,1.0,167,43.6,not_published
500 +Saint-Liguori,63065,Lanaudière,unifamilial,1.0,167,39.9,not_published
501 +Saint-Damien-de-Buckland,19030,Chaudière-Appalaches,unifamilial,1.0,167,43.3,not_published
502 +Cap-Chat,04047,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,167,40.5,not_published
503 +Upton,48038,Montérégie,unifamilial,1.0,167,43.3,not_published
504 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,unifamilial,1.0,166,39.5,not_published
505 +Dégelis,13005,Bas-Saint-Laurent,unifamilial,1.0,166,40.9,not_published
506 +Duhamel,80135,Outaouais,unifamilial,1.0,166,40.9,not_published
507 +Paspébiac,05032,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,165,41.9,not_published
508 +Saint-Malachie,19025,Chaudière-Appalaches,unifamilial,1.0,164,39.2,not_published
509 +Lac-Bouchette,91005,Saguenay–Lac-Saint-Jean,unifamilial,1.0,164,43.3,not_published
510 +Fermont,97035,Côte-Nord,unifamilial,2.0,164,10.7,not_published
511 +Saint-François-Xavier-de-Brompton,42020,Estrie,unifamilial,1.0,163,40.5,not_published
512 +Lambton,30095,Estrie,unifamilial,1.0,162,42.3,not_published
513 +Champlain,37220,Mauricie,unifamilial,1.0,162,37.5,not_published
514 +Sainte-Geneviève-de-Berthier,52040,Lanaudière,unifamilial,1.0,162,40.9,not_published
515 +Saint-Damase,54017,Montérégie,unifamilial,1.0,162,41.2,not_published
516 +Saint-Ferdinand,32013,Centre-du-Québec,unifamilial,1.0,161,38.8,not_published
517 +Dudswell,41117,Estrie,unifamilial,1.0,161,39.5,not_published
518 +Saint-Fabien,10070,Bas-Saint-Laurent,unifamilial,1.0,161,43.0,not_published
519 +Saint-Ours,53032,Montérégie,unifamilial,1.0,160,40.2,not_published
520 +Sainte-Cécile-de-Milton,47055,Estrie,unifamilial,1.0,160,39.9,not_published
521 +La Macaza,79047,Laurentides,unifamilial,1.0,159,39.2,not_published
522 +Saint-Ubalde,34090,Capitale-Nationale,unifamilial,1.0,159,39.9,not_published
523 +Saint-Marc-sur-Richelieu,57050,Montérégie,unifamilial,1.0,158,41.9,not_published
524 +Barraute,88022,Abitibi-Témiscamingue,unifamilial,1.0,158,38.5,not_published
525 +Mont-Saint-Hilaire,57035,Montérégie,condo,1.0,157,33.0,not_published
526 +Labrecque,93055,Saguenay–Lac-Saint-Jean,unifamilial,1.0,157,39.2,not_published
527 +Saint-Placide,72043,Laurentides,unifamilial,1.0,157,40.5,not_published
528 +Varennes,59020,Montérégie,condo,1.0,156,35.4,not_published
529 +Val-des-Monts,82015,Outaouais,plex,1.0,156,40.9,not_published
530 +La Prairie,67015,Montérégie,condo,1.0,156,30.6,not_published
531 +Forestville,95045,Côte-Nord,unifamilial,1.0,155,38.5,not_published
532 +Déléage,83070,Outaouais,unifamilial,1.0,155,38.5,not_published
533 +Saint-Cuthbert,52062,Lanaudière,unifamilial,1.0,155,39.9,not_published
534 +Saint-Ulric,08073,Bas-Saint-Laurent,unifamilial,1.0,154,38.8,not_published
535 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,plex,1.0,154,35.7,not_published
536 +Bonaventure,05045,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,153,36.4,not_published
537 +Prévost,75040,Laurentides,plex,1.0,152,38.1,not_published
538 +Boisbriand,73005,Laurentides,plex,1.0,152,37.5,not_published
539 +Sainte-Clotilde-de-Horton,39117,Centre-du-Québec,unifamilial,1.0,152,37.8,not_published
540 +L'Ancienne-Lorette,23057,Capitale-Nationale,plex,1.0,152,36.4,not_published
541 +Notre-Dame-du-Bon-Conseil,49075,Centre-du-Québec,unifamilial,1.0,151,38.8,not_published
542 +Marieville,55048,Montérégie,plex,1.0,151,36.8,not_published
543 +Saint-Nazaire,93045,Saguenay–Lac-Saint-Jean,unifamilial,1.0,151,37.8,not_published
544 +Saint-Amable,59015,Montérégie,plex,1.0,150,35.1,not_published
545 +Saint-Hubert-de-Rivière-du-Loup,12010,Bas-Saint-Laurent,unifamilial,1.0,150,39.2,not_published
546 +Saint-Martin,29045,Chaudière-Appalaches,unifamilial,1.0,150,38.1,not_published
547 +Saint-Constant,67035,Montérégie,plex,1.0,148,38.5,not_published
548 +Val-des-Lacs,78100,Laurentides,unifamilial,1.0,148,39.9,not_published
549 +Saint-Mathieu,67005,Montérégie,unifamilial,1.0,147,38.8,not_published
550 +Saint-Fulgence,94235,Saguenay–Lac-Saint-Jean,unifamilial,1.0,147,35.7,not_published
551 +Deux-Montagnes,72010,Laurentides,plex,1.0,146,38.5,not_published
552 +Chambord,91020,Saguenay–Lac-Saint-Jean,unifamilial,1.0,146,39.9,not_published
553 +Armagh,19037,Chaudière-Appalaches,unifamilial,1.0,146,38.1,not_published
554 +Montmagny,18050,Chaudière-Appalaches,plex,1.0,145,37.1,not_published
555 +Orford,45115,Estrie,condo,1.0,145,36.1,not_published
556 +Mirabel,74005,Laurentides,condo,1.0,145,33.0,not_published
557 +Rawdon,62037,Lanaudière,plex,1.0,144,37.1,not_published
558 +La Prairie,67015,Montérégie,plex,1.0,144,36.4,not_published
559 +Lavaltrie,52007,Lanaudière,plex,1.0,144,38.5,not_published
560 +Lantier,78015,Laurentides,unifamilial,1.0,144,38.5,not_published
561 +Lac-Sainte-Marie,83020,Outaouais,unifamilial,1.0,144,38.1,not_published
562 +Cayamant,83040,Outaouais,unifamilial,1.0,144,35.7,not_published
563 +Boisbriand,73005,Laurentides,condo,1.0,144,35.7,not_published
564 +Kingsey Falls,39097,Centre-du-Québec,unifamilial,1.0,143,35.7,not_published
565 +Coaticook,44037,Estrie,plex,1.0,143,37.8,not_published
566 +Saint-Victor,27008,Chaudière-Appalaches,unifamilial,1.0,142,36.1,not_published
567 +Yamaska,53072,Montérégie,unifamilial,1.0,142,35.1,not_published
568 +Bury,41070,Estrie,unifamilial,1.0,142,38.5,not_published
569 +Val-David,78010,Laurentides,plex,1.0,142,37.5,not_published
570 +Saint-Ignace-de-Loyola,52045,Lanaudière,unifamilial,1.0,142,35.4,not_published
571 +Grande-Rivière,02015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,141,38.5,not_published
572 +Roberval,91025,Saguenay–Lac-Saint-Jean,plex,1.0,141,37.5,not_published
573 +Hatley,45055,Estrie,unifamilial,1.0,141,35.7,not_published
574 +Les Escoumins,95025,Côte-Nord,unifamilial,1.0,140,34.4,not_published
575 +Chapais,99020,Nord-du-Québec,unifamilial,1.0,139,37.8,not_published
576 +Vaudreuil-Dorion,71083,Montérégie,plex,1.0,139,35.4,not_published
577 +Saint-Charles-sur-Richelieu,57057,Montérégie,unifamilial,1.0,139,35.1,not_published
578 +Shawville,84010,Outaouais,unifamilial,1.0,139,35.7,not_published
579 +Albanel,92030,Saguenay–Lac-Saint-Jean,unifamilial,1.0,138,34.0,not_published
580 +Saint-Félix-de-Kingsey,49005,Centre-du-Québec,unifamilial,1.0,138,37.1,not_published
581 +Causapscal,07018,Bas-Saint-Laurent,unifamilial,1.0,137,36.4,not_published
582 +Saint-Charles-Borromée,61035,Lanaudière,plex,1.0,137,37.1,not_published
583 +Sainte-Anne-des-Plaines,73035,Laurentides,plex,1.0,136,35.1,not_published
584 +Beaulac-Garthby,31008,Chaudière-Appalaches,unifamilial,1.0,136,36.1,not_published
585 +L'Île-Perrot,71060,Montérégie,plex,1.0,136,35.1,not_published
586 +Saint-Léonard-de-Portneuf,34115,Capitale-Nationale,unifamilial,1.0,136,38.5,not_published
587 +Lac-Mégantic,30030,Estrie,plex,1.0,136,35.7,not_published
588 +Sainte-Adèle,77022,Laurentides,condo,1.0,136,34.4,not_published
589 +Lac-des-Plages,80130,Outaouais,unifamilial,1.0,136,36.1,not_published
590 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,unifamilial,1.0,136,33.3,not_published
591 +Henryville,56042,Montérégie,unifamilial,1.0,135,33.7,not_published
592 +Rivière-Héva,89010,Abitibi-Témiscamingue,unifamilial,1.0,135,34.4,not_published
593 +Châteauguay,67050,Montérégie,condo,1.0,135,32.6,not_published
594 +Beaupré,21025,Capitale-Nationale,condo,1.0,134,34.7,not_published
595 +Pincourt,71070,Montérégie,condo,1.0,134,28.9,not_published
596 +Bois-des-Filion,73030,Laurentides,condo,1.0,134,30.6,not_published
597 +La Présentation,54035,Montérégie,unifamilial,1.0,134,34.0,not_published
598 +Sainte-Ursule,51040,Mauricie,unifamilial,1.0,134,37.5,not_published
599 +Stanstead,45025,Estrie,unifamilial,1.0,134,34.0,not_published
600 +Saint-Michel-de-Bellechasse,19110,Chaudière-Appalaches,unifamilial,1.0,133,36.4,not_published
601 +Sainte-Anne-de-la-Pérade,37205,Mauricie,unifamilial,1.0,132,34.4,not_published
602 +Les Éboulements,16048,Capitale-Nationale,unifamilial,1.0,132,36.4,not_published
603 +Saint-Pamphile,17010,Chaudière-Appalaches,unifamilial,1.0,132,34.4,not_published
604 +Saint-Alban,34097,Capitale-Nationale,unifamilial,1.0,131,34.0,not_published
605 +Otter Lake,84055,Outaouais,unifamilial,1.0,131,35.7,not_published
606 +Sainte-Christine-d'Auvergne,34105,Capitale-Nationale,unifamilial,1.0,129,34.7,not_published
607 +Caplan,05060,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,129,36.8,not_published
608 +La Malbaie,15013,Capitale-Nationale,plex,1.0,129,33.0,not_published
609 +Pointe-des-Cascades,71055,Montérégie,unifamilial,1.0,129,34.0,not_published
610 +Bowman,80145,Outaouais,unifamilial,1.0,128,32.3,not_published
611 +Louiseville,51015,Mauricie,plex,1.0,128,34.4,not_published
612 +Sainte-Hélène-de-Bagot,54095,Montérégie,unifamilial,1.0,127,32.3,not_published
613 +Valcourt,42055,Estrie,unifamilial,1.0,126,32.3,not_published
614 +Sayabec,07085,Bas-Saint-Laurent,unifamilial,1.0,126,33.7,not_published
615 +Sainte-Catherine,67030,Montérégie,plex,1.0,126,31.6,not_published
616 +Saint-Jacques-le-Mineur,68040,Montérégie,unifamilial,1.0,126,30.6,not_published
617 +Saint-Bernard-de-Lacolle,68005,Montérégie,unifamilial,1.0,126,33.0,not_published
618 +Tingwick,39025,Centre-du-Québec,unifamilial,1.0,126,36.4,not_published
619 +Grand-Remous,83095,Outaouais,unifamilial,1.0,126,34.0,not_published
620 +Charlemagne,60005,Lanaudière,condo,1.0,126,30.2,not_published
621 +Trois-Rives,35055,Mauricie,unifamilial,1.0,125,34.7,not_published
622 +Hérouxville,35035,Mauricie,unifamilial,1.0,125,33.0,not_published
623 +L'Épiphanie,60037,Lanaudière,plex,1.0,125,35.1,not_published
624 +Grenville,76055,Laurentides,unifamilial,1.0,125,33.0,not_published
625 +Montcalm,78055,Laurentides,unifamilial,1.0,125,33.3,not_published
626 +Windsor,42088,Estrie,plex,1.0,124,30.9,not_published
627 +Low,83010,Outaouais,unifamilial,1.0,124,32.0,not_published
628 +Frontenac,30025,Estrie,unifamilial,1.0,124,33.0,not_published
629 +Sainte-Justine,28045,Chaudière-Appalaches,unifamilial,1.0,124,32.0,not_published
630 +Bristol,84005,Outaouais,unifamilial,1.0,124,30.9,not_published
631 +Sainte-Madeleine,54025,Montérégie,unifamilial,1.0,124,31.6,not_published
632 +Saint-Alexandre-de-Kamouraska,14035,Bas-Saint-Laurent,unifamilial,1.0,124,30.9,not_published
633 +Saint-Robert,53020,Montérégie,unifamilial,1.0,123,33.3,not_published
634 +Saint-Albert,39085,Centre-du-Québec,unifamilial,1.0,122,33.3,not_published
635 +Saint-Herménégilde,44015,Estrie,unifamilial,1.0,122,32.3,not_published
636 +Price,09065,Bas-Saint-Laurent,unifamilial,1.0,122,31.3,not_published
637 +Donnacona,34025,Capitale-Nationale,plex,1.0,121,31.6,not_published
638 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,unifamilial,1.0,121,35.1,not_published
639 +Ayer's Cliff,45035,Estrie,unifamilial,1.0,121,33.3,not_published
640 +Cacouna,12057,Bas-Saint-Laurent,unifamilial,1.0,120,31.3,not_published
641 +Saint-Valérien-de-Milton,54065,Montérégie,unifamilial,1.0,120,33.7,not_published
642 +Wotton,40017,Estrie,unifamilial,1.0,120,33.7,not_published
643 +Contrecoeur,59035,Montérégie,plex,1.0,119,30.2,not_published
644 +Cleveland,42110,Estrie,unifamilial,1.0,119,32.6,not_published
645 +Saint-Félix-d'Otis,94225,Saguenay–Lac-Saint-Jean,unifamilial,1.0,119,32.6,not_published
646 +Lyster,32065,Centre-du-Québec,unifamilial,1.0,119,33.0,not_published
647 +Saint-Gervais,19075,Chaudière-Appalaches,unifamilial,1.0,119,33.7,not_published
648 +Senneterre,89045,Abitibi-Témiscamingue,unifamilial,1.0,119,34.0,not_published
649 +Saint-Louis-de-Blandford,39170,Centre-du-Québec,unifamilial,1.0,118,31.6,not_published
650 +Saint-Aubert,17055,Chaudière-Appalaches,unifamilial,1.0,118,28.5,not_published
651 +Notre-Dame-de-Pontmain,79010,Laurentides,unifamilial,1.0,118,34.0,not_published
652 +Bois-des-Filion,73030,Laurentides,plex,1.0,117,31.3,not_published
653 +Vallée-Jonction,26015,Chaudière-Appalaches,unifamilial,1.0,117,32.0,not_published
654 +Saint-Zacharie,28005,Chaudière-Appalaches,unifamilial,1.0,117,31.6,not_published
655 +Saint-Théodore-d'Acton,48045,Montérégie,unifamilial,1.0,117,30.6,not_published
656 +Saint-Esprit,63030,Lanaudière,unifamilial,1.0,117,31.3,not_published
657 +Frampton,26005,Chaudière-Appalaches,unifamilial,1.0,117,33.0,not_published
658 +Saint-Paulin,51060,Mauricie,unifamilial,1.0,117,34.4,not_published
659 +Port-Daniel–Gascons,02047,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,116,31.6,not_published
660 +Saint-Sauveur,77043,Laurentides,condo,1.0,116,32.0,not_published
661 +Saint-Clet,71045,Montérégie,unifamilial,1.0,116,32.0,not_published
662 +Pointe-aux-Outardes,96030,Côte-Nord,unifamilial,1.0,116,30.6,not_published
663 +L'Isle-Verte,12043,Bas-Saint-Laurent,unifamilial,1.0,116,29.6,not_published
664 +L'Île-Perrot,71060,Montérégie,condo,1.0,115,31.3,not_published
665 +Saint-Félicien,91042,Saguenay–Lac-Saint-Jean,plex,1.0,115,30.9,not_published
666 +Stukely-Sud,45105,Estrie,unifamilial,1.0,115,32.6,not_published
667 +Denholm,83005,Outaouais,unifamilial,1.0,115,30.9,not_published
668 +Les Îles-de-la-Madeleine,01023,Gaspésie–Îles-de-la-Madeleine,plex,1.0,114,31.3,not_published
669 +Disraeli,31020,Chaudière-Appalaches,unifamilial,1.0,114,32.3,not_published
670 +Cantley,82020,Outaouais,plex,1.0,114,30.6,not_published
671 +Carignan,57010,Montérégie,condo,1.0,114,31.3,not_published
672 +Chénéville,80103,Outaouais,unifamilial,1.0,113,33.7,not_published
673 +L'Ascension,79050,Laurentides,unifamilial,1.0,113,30.6,not_published
674 +Notre-Dame-de-Montauban,35005,Mauricie,unifamilial,1.0,113,32.3,not_published
675 +Saint-Alexis,63023,Lanaudière,unifamilial,1.0,113,30.9,not_published
676 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,unifamilial,1.0,113,29.2,not_published
677 +Contrecoeur,59035,Montérégie,condo,1.0,113,28.5,not_published
678 +Val-Joli,42095,Estrie,unifamilial,1.0,112,32.6,not_published
679 +Saint-Joachim-de-Shefford,47040,Estrie,unifamilial,1.0,112,28.5,not_published
680 +Sainte-Angèle-de-Monnoir,55030,Montérégie,unifamilial,1.0,112,28.9,not_published
681 +Eeyou Istchee Baie-James,99060,Nord-du-Québec,unifamilial,1.0,112,32.0,not_published
682 +Lac-Sergent,34120,Capitale-Nationale,unifamilial,1.0,112,31.3,not_published
683 +Mont-Carmel,14005,Bas-Saint-Laurent,unifamilial,1.0,112,31.6,not_published
684 +McMasterville,57025,Montérégie,plex,1.0,111,30.6,not_published
685 +Wentworth,76035,Laurentides,unifamilial,1.0,111,30.6,not_published
686 +Montréal-Est,66007,Montréal,unifamilial,1.0,111,30.9,not_published
687 +Saint-Claude,42100,Estrie,unifamilial,1.0,110,28.5,not_published
688 +Saint-Gabriel-de-Rimouski,09025,Bas-Saint-Laurent,unifamilial,1.0,110,30.2,not_published
689 +Saint-Jean-de-Dieu,11010,Bas-Saint-Laurent,unifamilial,1.0,110,29.2,not_published
690 +La Doré,91050,Saguenay–Lac-Saint-Jean,unifamilial,1.0,110,29.9,not_published
691 +Petite-Rivière-Saint-François,16005,Capitale-Nationale,unifamilial,1.0,110,30.2,not_published
692 +Sainte-Agathe-de-Lotbinière,33017,Chaudière-Appalaches,unifamilial,1.0,110,31.6,not_published
693 +Saint-Raymond,34128,Capitale-Nationale,plex,1.0,110,30.6,not_published
694 +Saint-Aimé-des-Lacs,15030,Capitale-Nationale,unifamilial,1.0,110,33.3,not_published
695 +Notre-Dame-de-la-Salette,80087,Outaouais,unifamilial,1.0,110,29.6,not_published
696 +Rivière-à-Pierre,34135,Capitale-Nationale,unifamilial,1.0,110,31.3,not_published
697 +Saint-Siméon,15058,Capitale-Nationale,unifamilial,1.0,109,28.9,not_published
698 +Saint-Narcisse-de-Rimouski,10015,Bas-Saint-Laurent,unifamilial,1.0,109,28.9,not_published
699 +Franklin,69010,Montérégie,unifamilial,1.0,109,30.6,not_published
700 +Saint-Hugues,54100,Montérégie,unifamilial,1.0,109,29.9,not_published
701 +Saint-Didace,52090,Lanaudière,unifamilial,1.0,108,29.9,not_published
702 +Pointe-Lebel,96025,Côte-Nord,unifamilial,1.0,108,26.5,not_published
703 +Saint-Mathieu-de-Rioux,11050,Bas-Saint-Laurent,unifamilial,1.0,107,29.9,not_published
704 +Saint-Justin,51045,Mauricie,unifamilial,1.0,107,30.9,not_published
705 +Varennes,59020,Montérégie,plex,1.0,107,30.9,not_published
706 +Notre-Dame-du-Portage,12080,Bas-Saint-Laurent,unifamilial,1.0,106,29.6,not_published
707 +Saint-Joachim,21020,Capitale-Nationale,unifamilial,1.0,106,29.2,not_published
708 +Saint-Éphrem-de-Beauce,29112,Chaudière-Appalaches,unifamilial,1.0,106,30.6,not_published
709 +Saint-Benoît-Labre,29100,Chaudière-Appalaches,unifamilial,1.0,106,28.5,not_published
710 +L'Assomption,60028,Lanaudière,condo,1.0,106,26.8,not_published
711 +Lamarche,93060,Saguenay–Lac-Saint-Jean,unifamilial,1.0,106,29.9,not_published
712 +Saint-Casimir,34078,Capitale-Nationale,unifamilial,1.0,105,31.6,not_published
713 +Sacré-Coeur,95010,Côte-Nord,unifamilial,1.0,105,29.9,not_published
714 +Saint-Armand,46017,Estrie,unifamilial,1.0,105,28.9,not_published
715 +Saint-Eugène,49105,Centre-du-Québec,unifamilial,1.0,105,28.9,not_published
716 +Saint-Adelphe,35015,Mauricie,unifamilial,1.0,105,28.5,not_published
717 +Sainte-Julie,59010,Montérégie,plex,1.0,105,29.6,not_published
718 +Nantes,30045,Estrie,unifamilial,1.0,105,29.6,not_published
719 +Saint-Pierre-les-Becquets,38065,Centre-du-Québec,unifamilial,1.0,104,26.1,not_published
720 +Batiscan,37210,Mauricie,unifamilial,1.0,104,27.8,not_published
721 +Sainte-Hénédine,26040,Chaudière-Appalaches,unifamilial,1.0,104,29.9,not_published
722 +Saint-Cyprien-de-Napierville,68035,Montérégie,unifamilial,1.0,104,30.6,not_published
723 +Saint-Narcisse,37240,Mauricie,unifamilial,1.0,103,26.8,not_published
724 +Montréal-Est,66007,Montréal,plex,1.0,103,29.6,not_published
725 +Mont-Joli,09077,Bas-Saint-Laurent,plex,1.0,103,28.2,not_published
726 +Bouchette,83050,Outaouais,unifamilial,1.0,103,31.3,not_published
727 +Sainte-Geneviève-de-Batiscan,37215,Mauricie,unifamilial,1.0,103,28.5,not_published
728 +L'Ancienne-Lorette,23057,Capitale-Nationale,condo,1.0,103,28.2,not_published
729 +Saint-Guillaume,49113,Centre-du-Québec,unifamilial,1.0,103,27.8,not_published
730 +Racine,42032,Estrie,unifamilial,1.0,102,27.8,not_published
731 +Boucherville,58033,Montérégie,plex,1.0,101,28.2,not_published
732 +Sainte-Brigide-d'Iberville,56105,Montérégie,unifamilial,1.0,101,29.2,not_published
733 +Blue Sea,83045,Outaouais,unifamilial,1.0,101,26.5,not_published
734 +Preissac,88090,Abitibi-Témiscamingue,unifamilial,1.0,100,27.8,not_published
735 +Saint-Simon,54090,Montérégie,unifamilial,1.0,100,28.5,not_published
736 +Matagami,99015,Nord-du-Québec,unifamilial,1.0,100,27.8,not_published
737 +Tring-Jonction,27060,Chaudière-Appalaches,unifamilial,1.0,100,25.1,not_published
738 +Brownsburg-Chatham,76043,Laurentides,plex,1.0,100,28.2,not_published
739 +Aumond,83090,Outaouais,unifamilial,1.0,99,28.5,not_published
740 +Notre-Dame-du-Nord,85090,Abitibi-Témiscamingue,unifamilial,1.0,99,29.6,not_published
741 +Sainte-Jeanne-d'Arc,92015,Saguenay–Lac-Saint-Jean,unifamilial,1.0,99,28.2,not_published
742 +Nicolet,50072,Centre-du-Québec,plex,1.0,99,27.5,not_published
743 +Terrasse-Vaudreuil,71075,Montérégie,unifamilial,1.0,99,28.5,not_published
744 +Kazabazua,83015,Outaouais,unifamilial,1.0,99,28.5,not_published
745 +Saint-Pacôme,14070,Bas-Saint-Laurent,unifamilial,1.0,99,27.8,not_published
746 +Rivière-Bleue,13025,Bas-Saint-Laurent,unifamilial,1.0,98,27.8,not_published
747 +Saint-Paul-de-Montminy,18030,Chaudière-Appalaches,unifamilial,1.0,98,25.8,not_published
748 +Saint-Patrice-de-Sherrington,68025,Montérégie,unifamilial,1.0,98,28.2,not_published
749 +Bromont,46078,Estrie,condo,1.0,98,27.8,not_published
750 +Nouvelle,06020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,98,28.5,not_published
751 +Desbiens,93005,Saguenay–Lac-Saint-Jean,unifamilial,1.0,97,27.1,not_published
752 +Saint-Jean-de-l'Île-d'Orléans,20015,Capitale-Nationale,unifamilial,1.0,97,26.8,not_published
753 +Saint-Joseph-de-Sorel,53050,Montérégie,unifamilial,1.0,97,26.8,not_published
754 +Roxton Falls,48010,Montérégie,unifamilial,1.0,97,25.4,not_published
755 +Clarendon,84015,Outaouais,unifamilial,1.0,97,28.2,not_published
756 +L'Avenir,49025,Centre-du-Québec,unifamilial,1.0,97,28.2,not_published
757 +Montebello,80010,Outaouais,unifamilial,1.0,97,28.9,not_published
758 +Sainte-Aurélie,28015,Chaudière-Appalaches,unifamilial,1.0,97,29.2,not_published
759 +Saint-Joseph-de-Sorel,53050,Montérégie,plex,1.0,96,26.5,not_published
760 +Frelighsburg,46010,Estrie,unifamilial,1.0,96,29.2,not_published
761 +Charlemagne,60005,Lanaudière,plex,1.0,96,28.9,not_published
762 +Thurso,80050,Outaouais,plex,1.0,96,27.5,not_published
763 +Kiamika,79025,Laurentides,unifamilial,1.0,96,26.5,not_published
764 +La Sarre,87090,Abitibi-Témiscamingue,plex,1.0,96,28.9,not_published
765 +Sainte-Perpétue,17030,Chaudière-Appalaches,unifamilial,1.0,96,27.1,not_published
766 +Saint-Georges-de-Windsor,40032,Estrie,unifamilial,1.0,95,27.5,not_published
767 +Notre-Dame-des-Neiges,11045,Bas-Saint-Laurent,unifamilial,1.0,95,26.8,not_published
768 +Saint-Jude,54110,Montérégie,unifamilial,1.0,94,25.8,not_published
769 +Saint-Benjamin,28025,Chaudière-Appalaches,unifamilial,1.0,94,26.1,not_published
770 +Berthierville,52035,Lanaudière,plex,1.0,94,25.1,not_published
771 +Saint-Lambert,58012,Montérégie,plex,1.0,94,26.1,not_published
772 +Saint-René-de-Matane,08035,Bas-Saint-Laurent,unifamilial,1.0,94,29.9,not_published
773 +Saint-Édouard-de-Lotbinière,33080,Chaudière-Appalaches,unifamilial,1.0,94,27.5,not_published
774 +Marieville,55048,Montérégie,condo,1.0,93,25.8,not_published
775 +Saint-Louis,54120,Montérégie,unifamilial,1.0,93,27.5,not_published
776 +East Angus,41060,Estrie,plex,1.0,93,28.2,not_published
777 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,condo,1.0,93,24.7,not_published
778 +Saint-Aimé-du-Lac-des-Îles,79022,Laurentides,unifamilial,1.0,93,27.8,not_published
779 +Lorrainville,85037,Abitibi-Témiscamingue,unifamilial,1.0,93,28.5,not_published
780 +La Patrie,41027,Estrie,unifamilial,1.0,93,27.1,not_published
781 +Inverness,32058,Centre-du-Québec,unifamilial,1.0,92,26.5,not_published
782 +Palmarolle,87025,Abitibi-Témiscamingue,unifamilial,1.0,92,27.1,not_published
783 +Saint-Basile-le-Grand,57020,Montérégie,condo,1.0,92,22.7,not_published
784 +La Guadeloupe,29030,Chaudière-Appalaches,unifamilial,1.0,92,24.7,not_published
785 +Lac-Saguay,79060,Laurentides,unifamilial,1.0,91,26.5,not_published
786 +Sainte-Rose-de-Watford,28030,Chaudière-Appalaches,unifamilial,1.0,91,25.4,not_published
787 +Saint-Léon-de-Standon,19020,Chaudière-Appalaches,unifamilial,1.0,91,26.1,not_published
788 +Notre-Dame-Auxiliatrice-de-Buckland,19010,Chaudière-Appalaches,unifamilial,1.0,91,26.1,not_published
789 +Mont-Blanc,78047,Laurentides,plex,1.0,90,25.1,not_published
790 +Sainte-Marguerite,26035,Chaudière-Appalaches,unifamilial,1.0,90,24.4,not_published
791 +Waterloo,47025,Estrie,plex,1.0,90,24.4,not_published
792 +Saint-Nérée-de-Bellechasse,19045,Chaudière-Appalaches,unifamilial,1.0,90,26.1,not_published
793 +Marston,30035,Estrie,unifamilial,1.0,89,26.1,not_published
794 +Saint-Philémon,19005,Chaudière-Appalaches,unifamilial,1.0,89,26.1,not_published
795 +Drummondville,49058,Centre-du-Québec,condo,1.0,89,25.1,published
796 +Saint-Cyrille-de-Lessard,17045,Chaudière-Appalaches,unifamilial,1.0,89,26.8,not_published
797 +Val-Alain,33070,Chaudière-Appalaches,unifamilial,1.0,88,23.7,not_published
798 +Saint-Odilon-de-Cranbourne,27035,Chaudière-Appalaches,unifamilial,1.0,88,25.1,not_published
799 +Sainte-Sophie-de-Lévrard,38040,Centre-du-Québec,unifamilial,1.0,88,26.8,not_published
800 +Saint-Bruno-de-Guigues,85045,Abitibi-Témiscamingue,unifamilial,1.0,88,24.4,not_published
801 +Godmanchester,69060,Montérégie,unifamilial,1.0,88,26.8,not_published
802 +Lac-du-Cerf,79015,Laurentides,unifamilial,1.0,88,25.1,not_published
803 +Richmond,42098,Estrie,plex,1.0,87,25.1,not_published
804 +La Corne,88030,Abitibi-Témiscamingue,unifamilial,1.0,87,23.4,not_published
805 +Très-Saint-Rédempteur,71125,Montérégie,unifamilial,1.0,87,26.5,not_published
806 +Saint-Étienne-de-Bolton,45100,Estrie,unifamilial,1.0,86,24.4,not_published
807 +Sainte-Flavie,09085,Bas-Saint-Laurent,unifamilial,1.0,86,25.4,not_published
808 +Saint-Urbain-Premier,70005,Montérégie,unifamilial,1.0,86,23.4,not_published
809 +Brébeuf,78075,Laurentides,unifamilial,1.0,86,25.1,not_published
810 +Ham-Nord,39010,Centre-du-Québec,unifamilial,1.0,85,25.4,not_published
811 +Saint-Ludger-de-Milot,93080,Saguenay–Lac-Saint-Jean,unifamilial,1.0,85,25.1,not_published
812 +Saint-Valère,39135,Centre-du-Québec,unifamilial,1.0,85,25.1,not_published
813 +Sorel-Tracy,53052,Montérégie,condo,1.0,85,26.5,not_published
814 +Saint-René,29050,Chaudière-Appalaches,unifamilial,1.0,84,24.4,not_published
815 +Ragueneau,96040,Côte-Nord,unifamilial,1.0,84,24.4,not_published
816 +North Hatley,45050,Estrie,unifamilial,1.0,84,26.8,not_published
817 +Baie-Saint-Paul,16013,Capitale-Nationale,plex,1.0,84,25.4,not_published
818 +Saint-Fabien-de-Panet,18015,Chaudière-Appalaches,unifamilial,1.0,84,25.1,not_published
819 +Deschaillons-sur-Saint-Laurent,38070,Centre-du-Québec,unifamilial,1.0,84,24.7,not_published
820 +Saint-Édouard-de-Maskinongé,51050,Mauricie,unifamilial,1.0,84,22.7,not_published
821 +Lac-Brome,46075,Estrie,condo,1.0,84,25.1,not_published
822 +Saint-Rémi,68055,Montérégie,condo,1.0,84,21.3,not_published
823 +Saint-Magloire,28075,Chaudière-Appalaches,unifamilial,1.0,83,24.7,not_published
824 +Saint-Norbert-d'Arthabaska,39043,Centre-du-Québec,unifamilial,1.0,83,24.7,not_published
825 +Plaisance,80045,Outaouais,unifamilial,1.0,83,25.4,not_published
826 +Saint-Constant,67035,Montérégie,condo,1.0,83,17.9,not_published
827 +Fort-Coulonge,84060,Outaouais,unifamilial,1.0,83,24.7,not_published
828 +Grande-Vallée,03020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,83,24.4,not_published
829 +Saint-Hilarion,16050,Capitale-Nationale,unifamilial,1.0,83,23.0,not_published
830 +Notre-Dame-des-Prairies,61030,Lanaudière,plex,1.0,83,23.7,not_published
831 +Saint-Joseph-du-Lac,72025,Laurentides,plex,1.0,82,22.7,not_published
832 +Dosquet,33040,Chaudière-Appalaches,unifamilial,1.0,82,24.1,not_published
833 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,plex,1.0,82,23.7,not_published
834 +Saint-Valérien,10060,Bas-Saint-Laurent,unifamilial,1.0,82,24.4,not_published
835 +Malartic,89015,Abitibi-Témiscamingue,plex,1.0,82,23.4,not_published
836 +Sainte-Eulalie,50005,Centre-du-Québec,unifamilial,1.0,82,25.4,not_published
837 +Saint-Camille-de-Lellis,28070,Chaudière-Appalaches,unifamilial,1.0,82,24.1,not_published
838 +Saint-Rémi,68055,Montérégie,plex,1.0,82,25.1,not_published
839 +Girardville,92055,Saguenay–Lac-Saint-Jean,unifamilial,1.0,81,22.7,not_published
840 +Lotbinière,33115,Chaudière-Appalaches,unifamilial,1.0,81,24.7,not_published
841 +Laurierville,32072,Centre-du-Québec,unifamilial,1.0,81,24.4,not_published
842 +Vaudreuil-sur-le-Lac,71090,Montérégie,unifamilial,1.0,81,24.4,not_published
843 +Saint-Donat,09030,Bas-Saint-Laurent,unifamilial,1.0,81,22.7,not_published
844 +Sainte-Monique,93075,Saguenay–Lac-Saint-Jean,unifamilial,1.0,81,22.3,not_published
845 +Saint-Honoré-de-Shenley,29038,Chaudière-Appalaches,unifamilial,1.0,81,22.3,not_published
846 +Saint-Ludger,30072,Estrie,unifamilial,1.0,80,24.7,not_published
847 +Saint-Janvier-de-Joly,33065,Chaudière-Appalaches,unifamilial,1.0,80,24.4,not_published
848 +Carignan,57010,Montérégie,plex,1.0,80,22.7,not_published
849 +Saint-Flavien,33052,Chaudière-Appalaches,unifamilial,1.0,80,21.3,not_published
850 +Saint-Wenceslas,50023,Centre-du-Québec,unifamilial,1.0,79,22.7,not_published
851 +Saint-Majorique-de-Grantham,49095,Centre-du-Québec,unifamilial,1.0,79,24.1,not_published
852 +Gaspé,03005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,79,24.1,not_published
853 +Val-Brillant,07080,Bas-Saint-Laurent,unifamilial,1.0,79,21.6,not_published
854 +Saint-Siméon,05055,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,79,22.3,not_published
855 +Saint-Césaire,55023,Montérégie,plex,1.0,79,20.3,not_published
856 +New Carlisle,05040,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,79,24.1,not_published
857 +L'Isle-aux-Coudres,16023,Capitale-Nationale,unifamilial,1.0,79,23.4,not_published
858 +Saint-Barnabé,51025,Mauricie,unifamilial,1.0,79,22.0,not_published
859 +Manseau,38028,Centre-du-Québec,unifamilial,1.0,79,22.3,not_published
860 +Lac-Delage,22030,Capitale-Nationale,unifamilial,1.0,79,21.0,not_published
861 +Plessisville,32043,Centre-du-Québec,plex,1.0,78,21.6,not_published
862 +Saint-Donat,62060,Lanaudière,plex,1.0,78,23.4,not_published
863 +Lac-Drolet,30080,Estrie,unifamilial,1.0,78,22.7,not_published
864 +Bécancour,38010,Centre-du-Québec,plex,1.0,78,22.3,not_published
865 +Acton Vale,48028,Montérégie,plex,1.0,78,22.3,not_published
866 +Saint-Norbert,52070,Lanaudière,unifamilial,1.0,78,23.4,not_published
867 +Lac-au-Saumon,07057,Bas-Saint-Laurent,unifamilial,1.0,78,23.4,not_published
868 +Hemmingford,68010,Montérégie,unifamilial,1.0,77,22.3,not_published
869 +Saint-Luc-de-Bellechasse,28060,Chaudière-Appalaches,unifamilial,1.0,77,22.3,not_published
870 +Saint-François-de-la-Rivière-du-Sud,18060,Chaudière-Appalaches,unifamilial,1.0,77,22.7,not_published
871 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,plex,1.0,77,23.7,not_published
872 +Saint-Édouard,68045,Montérégie,unifamilial,1.0,76,21.6,not_published
873 +Saint-Henri-de-Taillon,93070,Saguenay–Lac-Saint-Jean,unifamilial,1.0,76,24.4,not_published
874 +Charette,51080,Mauricie,unifamilial,1.0,76,23.7,not_published
875 +Saint-Isidore-de-Clifton,41012,Estrie,unifamilial,1.0,76,22.3,not_published
876 +Chibougamau,99025,Nord-du-Québec,plex,1.0,76,21.3,not_published
877 +Saint-Félix-de-Valois,62007,Lanaudière,plex,1.0,76,21.3,not_published
878 +Boileau,80115,Outaouais,unifamilial,1.0,76,21.3,not_published
879 +Crabtree,61013,Lanaudière,plex,1.0,76,21.6,not_published
880 +Chute-aux-Outardes,96035,Côte-Nord,unifamilial,1.0,76,21.6,not_published
881 +Saint-Thomas-Didyme,92045,Saguenay–Lac-Saint-Jean,unifamilial,1.0,76,22.7,not_published
882 +Rivière-Ouelle,14065,Bas-Saint-Laurent,unifamilial,1.0,76,21.6,not_published
883 +Laverlochère-Angliers,85052,Abitibi-Témiscamingue,unifamilial,1.0,76,21.0,not_published
884 +Saint-Épiphane,12030,Bas-Saint-Laurent,unifamilial,1.0,76,22.7,not_published
885 +Sainte-Élisabeth,52030,Lanaudière,unifamilial,1.0,75,20.6,not_published
886 +Chesterville,39030,Centre-du-Québec,unifamilial,1.0,75,19.9,not_published
887 +Côte-Saint-Luc,66058,Montréal,plex,1.0,75,22.7,not_published
888 +Sainte-Thérèse-de-la-Gatineau,83055,Outaouais,unifamilial,1.0,75,23.4,not_published
889 +Sainte-Pétronille,20030,Capitale-Nationale,unifamilial,1.0,75,22.3,not_published
890 +Sutton,46058,Estrie,plex,1.0,75,22.7,not_published
891 +Sainte-Marie-Salomé,63005,Lanaudière,unifamilial,1.0,75,22.0,not_published
892 +Saint-Émile-de-Suffolk,80125,Outaouais,unifamilial,1.0,75,20.6,not_published
893 +Saint-Georges,29073,Chaudière-Appalaches,condo,1.0,75,19.9,not_published
894 +Richelieu,55057,Montérégie,plex,1.0,74,22.7,not_published
895 +Sainte-Anne-du-Lac,79115,Laurentides,unifamilial,1.0,74,23.7,not_published
896 +Maniwaki,83065,Outaouais,plex,1.0,74,22.0,not_published
897 +Namur,80110,Outaouais,unifamilial,1.0,73,20.6,not_published
898 +Notre-Dame-de-la-Paix,80020,Outaouais,unifamilial,1.0,73,21.0,not_published
899 +Amqui,07047,Bas-Saint-Laurent,plex,1.0,73,19.9,not_published
900 +Saint-Séverin,35020,Mauricie,unifamilial,1.0,73,20.6,not_published
901 +Hatley,45043,Estrie,unifamilial,1.0,73,22.0,not_published
902 +Bromont,46078,Estrie,plex,1.0,73,22.7,not_published
903 +Sainte-Anne-des-Monts,04037,Gaspésie–Îles-de-la-Madeleine,plex,1.0,73,20.3,not_published
904 +Saint-Rémi-de-Tingwick,39020,Centre-du-Québec,unifamilial,1.0,73,21.0,not_published
905 +Baie-du-Febvre,50100,Centre-du-Québec,unifamilial,1.0,72,20.6,not_published
906 +Dupuy,87085,Abitibi-Témiscamingue,unifamilial,1.0,72,21.0,not_published
907 +Trécesson,88075,Abitibi-Témiscamingue,unifamilial,1.0,72,22.7,not_published
908 +Estérel,77011,Laurentides,unifamilial,1.0,72,21.6,not_published
909 +Lac-Saint-Paul,79105,Laurentides,unifamilial,1.0,72,21.6,not_published
910 +Saint-Louis-du-Ha! Ha!,13080,Bas-Saint-Laurent,unifamilial,1.0,72,22.0,not_published
911 +Saint-Irénée,15005,Capitale-Nationale,unifamilial,1.0,72,22.7,not_published
912 +Sainte-Sabine,46105,Estrie,unifamilial,1.0,71,21.3,not_published
913 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,plex,1.0,71,21.6,not_published
914 +Saint-Roch-de-Mékinac,35045,Mauricie,unifamilial,1.0,71,21.0,not_published
915 +Grandes-Piles,35040,Mauricie,unifamilial,1.0,71,19.9,not_published
916 +Roxton,48015,Montérégie,unifamilial,1.0,71,21.3,not_published
917 +Cookshire-Eaton,41038,Estrie,plex,1.0,71,22.3,not_published
918 +Beauceville,27028,Chaudière-Appalaches,plex,1.0,71,22.0,not_published
919 +Saint-Marc-de-Figuery,88040,Abitibi-Témiscamingue,unifamilial,1.0,71,21.3,not_published
920 +Bolton-Ouest,46065,Estrie,unifamilial,1.0,71,22.0,not_published
921 +Sainte-Hedwidge,91030,Saguenay–Lac-Saint-Jean,unifamilial,1.0,71,21.0,not_published
922 +Hébertville,93022,Saguenay–Lac-Saint-Jean,plex,1.0,71,21.6,not_published
923 +Saint-Modeste,12020,Bas-Saint-Laurent,unifamilial,1.0,71,19.2,not_published
924 +Durham-Sud,49015,Centre-du-Québec,unifamilial,1.0,71,19.9,not_published
925 +Huberdeau,78065,Laurentides,unifamilial,1.0,70,20.3,not_published
926 +Morin-Heights,77050,Laurentides,plex,1.0,70,19.9,not_published
927 +Salaberry-de-Valleyfield,70052,Montérégie,condo,1.0,70,20.6,not_published
928 +Saint-Gabriel,52080,Lanaudière,plex,1.0,70,20.6,not_published
929 +Très-Saint-Sacrement,69030,Montérégie,unifamilial,1.0,70,20.3,not_published
930 +Howick,69025,Montérégie,unifamilial,1.0,70,19.9,not_published
931 +Montréal-Est,66007,Montréal,condo,1.0,70,21.3,not_published
932 +Saint-Cyprien,12005,Bas-Saint-Laurent,unifamilial,1.0,70,19.2,not_published
933 +Landrienne,88035,Abitibi-Témiscamingue,unifamilial,1.0,70,19.9,not_published
934 +Mulgrave-et-Derry,80085,Outaouais,unifamilial,1.0,69,19.9,not_published
935 +Bégin,94250,Saguenay–Lac-Saint-Jean,unifamilial,1.0,69,20.3,not_published
936 +Sainte-Praxède,31050,Chaudière-Appalaches,unifamilial,1.0,69,20.3,not_published
937 +Kirkland,66102,Montréal,condo,1.0,69,19.2,not_published
938 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,unifamilial,1.0,69,20.3,not_published
939 +Joliette,61025,Lanaudière,condo,1.0,68,17.9,not_published
940 +La Pêche,82035,Outaouais,plex,1.0,68,19.9,not_published
941 +Saint-Étienne-de-Beauharnois,70030,Montérégie,unifamilial,1.0,68,21.6,not_published
942 +Mayo,80065,Outaouais,unifamilial,1.0,68,19.2,not_published
943 +Valcourt,42060,Estrie,unifamilial,1.0,68,19.6,not_published
944 +Mont-Saint-Michel,79110,Laurentides,unifamilial,1.0,68,21.3,not_published
945 +Saint-Urbain,16055,Capitale-Nationale,unifamilial,1.0,68,19.9,not_published
946 +Saint-Roch-des-Aulnaies,17065,Chaudière-Appalaches,unifamilial,1.0,68,20.6,not_published
947 +Sainte-Justine-de-Newton,71115,Montérégie,unifamilial,1.0,68,18.9,not_published
948 +Saint-Théophile,29005,Chaudière-Appalaches,unifamilial,1.0,68,21.6,not_published
949 +Labelle,78120,Laurentides,plex,1.0,68,19.9,not_published
950 +Sainte-Félicité,08023,Bas-Saint-Laurent,unifamilial,1.0,67,19.2,not_published
951 +Béarn,85020,Abitibi-Témiscamingue,unifamilial,1.0,67,19.6,not_published
952 +Rigaud,71133,Montérégie,plex,1.0,67,19.6,not_published
953 +Pont-Rouge,34017,Capitale-Nationale,plex,1.0,67,18.9,not_published
954 +Sainte-Cécile-de-Whitton,30050,Estrie,unifamilial,1.0,66,20.3,not_published
955 +La Durantaye,19090,Chaudière-Appalaches,unifamilial,1.0,66,19.9,not_published
956 +Melbourne,42075,Estrie,unifamilial,1.0,66,18.9,not_published
957 +La Bostonnais,90017,Mauricie,unifamilial,1.0,66,19.2,not_published
958 +Saint-Zotique,71025,Montérégie,condo,1.0,66,19.6,not_published
959 +Thorne,84045,Outaouais,unifamilial,1.0,66,20.3,not_published
960 +Sainte-Marie,26030,Chaudière-Appalaches,plex,1.0,66,19.9,not_published
961 +Saint-Marcellin,10025,Bas-Saint-Laurent,unifamilial,1.0,65,19.2,not_published
962 +Saints-Anges,26010,Chaudière-Appalaches,unifamilial,1.0,65,21.0,not_published
963 +Litchfield,84040,Outaouais,unifamilial,1.0,65,19.2,not_published
964 +Saint-Pie,54008,Montérégie,plex,1.0,65,19.6,not_published
965 +Saint-Zotique,71025,Montérégie,plex,1.0,65,20.3,not_published
966 +Saint-Sylvestre,33007,Chaudière-Appalaches,unifamilial,1.0,65,19.9,not_published
967 +Saint-Pierre-Baptiste,32050,Centre-du-Québec,unifamilial,1.0,64,20.3,not_published
968 +Mercier,67045,Montérégie,plex,1.0,64,17.2,not_published
969 +Saint-Jacques,63013,Lanaudière,plex,1.0,64,19.6,not_published
970 +Saint-Tite,35027,Mauricie,plex,1.0,64,19.6,not_published
971 +Princeville,32033,Centre-du-Québec,plex,1.0,64,18.6,not_published
972 +Havelock,69005,Montérégie,unifamilial,1.0,64,19.6,not_published
973 +Piedmont,77030,Laurentides,plex,1.0,64,20.3,not_published
974 +Lefebvre,49020,Centre-du-Québec,unifamilial,1.0,64,17.2,not_published
975 +Bonsecours,42040,Estrie,unifamilial,1.0,64,18.9,not_published
976 +Saint-Léon-le-Grand,07030,Bas-Saint-Laurent,unifamilial,1.0,64,17.9,not_published
977 +Westmount,66032,Montréal,plex,1.0,64,18.9,not_published
978 +Saint-Romain,30100,Estrie,unifamilial,1.0,63,17.2,not_published
979 +Massueville,53010,Montérégie,unifamilial,1.0,63,19.9,not_published
980 +Trois-Pistoles,11040,Bas-Saint-Laurent,plex,1.0,63,18.6,not_published
981 +Saint-Maxime-du-Mont-Louis,04010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,63,18.6,not_published
982 +Saint-Narcisse-de-Beaurivage,33030,Chaudière-Appalaches,unifamilial,1.0,63,18.9,not_published
983 +Saint-Patrice-de-Beaurivage,33025,Chaudière-Appalaches,unifamilial,1.0,62,16.5,not_published
984 +Saint-Rosaire,39145,Centre-du-Québec,unifamilial,1.0,62,19.2,not_published
985 +Duparquet,87005,Abitibi-Témiscamingue,unifamilial,1.0,62,19.9,not_published
986 +Ogden,45020,Estrie,unifamilial,1.0,62,18.9,not_published
987 +Cloridorme,03010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,62,20.3,not_published
988 +Dixville,44023,Estrie,unifamilial,1.0,62,19.2,not_published
989 +Saint-Calixte,63055,Lanaudière,plex,1.0,62,17.9,not_published
990 +Lac-Édouard,90027,Mauricie,unifamilial,1.0,62,19.6,not_published
991 +Saint-Stanislas,37245,Mauricie,unifamilial,1.0,61,18.2,not_published
992 +Sainte-Hélène-de-Kamouraska,14025,Bas-Saint-Laurent,unifamilial,1.0,61,18.2,not_published
993 +Westbury,41065,Estrie,unifamilial,1.0,61,19.2,not_published
994 +Saint-Denis-De La Bouteillerie,14055,Bas-Saint-Laurent,unifamilial,1.0,61,17.9,not_published
995 +Longue-Rive,95032,Côte-Nord,unifamilial,1.0,61,18.9,not_published
996 +Saint-Paul,61005,Lanaudière,plex,1.0,61,16.8,not_published
997 +Saint-Bonaventure,49125,Centre-du-Québec,unifamilial,1.0,61,17.9,not_published
998 +Pointe-à-la-Croix,06030,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,61,18.9,not_published
999 +Saint-David,53005,Montérégie,unifamilial,1.0,61,17.5,not_published
1000 +Saint-Cyprien,28040,Chaudière-Appalaches,unifamilial,1.0,61,17.5,not_published
1001 +Mont-Saint-Hilaire,57035,Montérégie,plex,1.0,61,19.9,not_published
1002 +Lanoraie,52017,Lanaudière,plex,1.0,61,18.6,not_published
1003 +Saint-Philippe,67010,Montérégie,condo,1.0,60,17.5,not_published
1004 +Métis-sur-Mer,09048,Bas-Saint-Laurent,unifamilial,1.0,60,17.9,not_published
1005 +Lac-Saint-Joseph,22015,Capitale-Nationale,unifamilial,1.0,60,16.8,not_published
1006 +Saint-Célestin,50030,Centre-du-Québec,unifamilial,1.0,60,18.9,not_published
1007 +Sainte-Julienne,63060,Lanaudière,plex,1.0,59,16.8,not_published
1008 +Newport,41037,Estrie,unifamilial,1.0,59,18.6,not_published
1009 +Saint-Frédéric,27065,Chaudière-Appalaches,unifamilial,1.0,59,17.2,not_published
1010 +Shannon,22020,Capitale-Nationale,plex,1.0,59,18.2,not_published
1011 +L'Île-du-Grand-Calumet,84035,Outaouais,unifamilial,1.0,59,18.9,not_published
1012 +Arundel,78060,Laurentides,unifamilial,1.0,59,17.5,not_published
1013 +Alleyn-et-Cawood,84050,Outaouais,unifamilial,1.0,59,17.2,not_published
1014 +Saint-Barnabé-Sud,54105,Montérégie,unifamilial,1.0,59,18.6,not_published
1015 +Clermont,15035,Capitale-Nationale,plex,1.0,59,18.2,not_published
1016 +Fassett,80005,Outaouais,unifamilial,1.0,58,17.5,not_published
1017 +Gallichan,87020,Abitibi-Témiscamingue,unifamilial,1.0,58,18.6,not_published
1018 +Montcerf-Lytton,83088,Outaouais,unifamilial,1.0,58,15.1,not_published
1019 +Sainte-Germaine-Boulé,87030,Abitibi-Témiscamingue,unifamilial,1.0,58,17.9,not_published
1020 +Saint-Lazare-de-Bellechasse,19050,Chaudière-Appalaches,unifamilial,1.0,58,17.5,not_published
1021 +Bécancour,38010,Centre-du-Québec,condo,1.0,58,15.5,not_published
1022 +Sainte-Marthe,71110,Montérégie,unifamilial,1.0,58,18.6,not_published
1023 +Ferland-et-Boilleau,94220,Saguenay–Lac-Saint-Jean,unifamilial,1.0,58,17.5,not_published
1024 +Saint-Simon-de-Rimouski,11055,Bas-Saint-Laurent,unifamilial,1.0,57,18.2,not_published
1025 +Pointe-Calumet,72020,Laurentides,plex,1.0,57,16.8,not_published
1026 +Taschereau,87042,Abitibi-Témiscamingue,unifamilial,1.0,57,18.2,not_published
1027 +Saint-Arsène,12065,Bas-Saint-Laurent,unifamilial,1.0,56,16.5,not_published
1028 +Sainte-Rose-du-Nord,94230,Saguenay–Lac-Saint-Jean,unifamilial,1.0,56,16.5,not_published
1029 +Notre-Dame-du-Bon-Conseil,49080,Centre-du-Québec,unifamilial,1.0,56,16.8,not_published
1030 +Sutton,46058,Estrie,condo,1.0,56,15.8,not_published
1031 +Stanbridge East,46045,Estrie,unifamilial,1.0,56,16.5,not_published
1032 +Grosses-Roches,08015,Bas-Saint-Laurent,unifamilial,1.0,56,17.9,not_published
1033 +Packington,13015,Bas-Saint-Laurent,unifamilial,1.0,56,17.5,not_published
1034 +Rivière-Rouge,79037,Laurentides,plex,1.0,56,18.2,not_published
1035 +Duhamel-Ouest,85030,Abitibi-Témiscamingue,unifamilial,1.0,55,17.2,not_published
1036 +Saint-Pierre-de-Broughton,31135,Chaudière-Appalaches,unifamilial,1.0,55,17.2,not_published
1037 +Saint-Léon-le-Grand,51035,Mauricie,unifamilial,1.0,55,16.8,not_published
1038 +Sainte-Christine,48020,Montérégie,unifamilial,1.0,55,17.5,not_published
1039 +Prévost,75040,Laurentides,condo,1.0,55,16.2,not_published
1040 +Saint-Nazaire-d'Acton,48050,Montérégie,unifamilial,1.0,55,17.2,not_published
1041 +Saint-Jacques-de-Leeds,31140,Chaudière-Appalaches,unifamilial,1.0,55,16.5,not_published
1042 +Saint-Édouard-de-Fabre,85015,Abitibi-Témiscamingue,unifamilial,1.0,54,16.5,not_published
1043 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,plex,1.0,54,16.2,not_published
1044 +Amos,88057,Abitibi-Témiscamingue,plex,1.0,54,14.8,not_published
1045 +Saint-Vallier,19117,Chaudière-Appalaches,unifamilial,1.0,54,16.5,not_published
1046 +Métabetchouan–Lac-à-la-Croix,93012,Saguenay–Lac-Saint-Jean,plex,1.0,54,16.5,not_published
1047 +Saint-Charles-de-Bourget,94260,Saguenay–Lac-Saint-Jean,unifamilial,1.0,54,15.8,not_published
1048 +Courcelles–Saint-Évariste,29027,Chaudière-Appalaches,unifamilial,1.0,54,15.8,not_published
1049 +Piopolis,30020,Estrie,unifamilial,1.0,53,16.5,not_published
1050 +L'Ange-Gardien,82005,Outaouais,plex,1.0,53,16.5,not_published
1051 +La Pocatière,14082,Bas-Saint-Laurent,plex,1.0,53,17.9,not_published
1052 +Les Bergeronnes,95018,Côte-Nord,unifamilial,1.0,53,17.5,not_published
1053 +Sainte-Anne-des-Lacs,77035,Laurentides,plex,1.0,53,16.5,not_published
1054 +Les Coteaux,71033,Montérégie,plex,1.0,53,15.5,not_published
1055 +Notre-Dame-du-Rosaire,18040,Chaudière-Appalaches,unifamilial,1.0,53,14.8,not_published
1056 +Saint-Mathieu-d'Harricana,88050,Abitibi-Témiscamingue,unifamilial,1.0,53,15.5,not_published
1057 +Danville,40047,Estrie,plex,1.0,53,14.1,not_published
1058 +Pointe-Fortune,71140,Montérégie,unifamilial,1.0,53,16.8,not_published
1059 +Saint-Just-de-Bretenières,18005,Chaudière-Appalaches,unifamilial,1.0,52,15.8,not_published
1060 +Sainte-Martine,70012,Montérégie,condo,1.0,52,15.1,not_published
1061 +Sainte-Angèle-de-Prémont,51055,Mauricie,unifamilial,1.0,52,15.8,not_published
1062 +Saint-Sébastien,30085,Estrie,unifamilial,1.0,52,16.8,not_published
1063 +Saint-Pierre-de-la-Rivière-du-Sud,18055,Chaudière-Appalaches,unifamilial,1.0,52,16.8,not_published
1064 +Saint-Michel-du-Squatec,13065,Bas-Saint-Laurent,unifamilial,1.0,52,14.4,not_published
1065 +Chandler,02028,Gaspésie–Îles-de-la-Madeleine,plex,1.0,52,16.8,not_published
1066 +Saint-André-Avellin,80027,Outaouais,plex,1.0,52,15.5,not_published
1067 +Péribonka,92010,Saguenay–Lac-Saint-Jean,unifamilial,1.0,51,17.2,not_published
1068 +Parisville,38055,Centre-du-Québec,unifamilial,1.0,51,15.8,not_published
1069 +Lac-Beauport,22040,Capitale-Nationale,plex,1.0,51,15.8,not_published
1070 +Saint-Zéphirin-de-Courval,50090,Centre-du-Québec,unifamilial,1.0,51,16.5,not_published
1071 +Beaupré,21025,Capitale-Nationale,plex,1.0,51,16.5,not_published
1072 +Val-Morin,78005,Laurentides,plex,1.0,51,15.1,not_published
1073 +Ville-Marie,85025,Abitibi-Témiscamingue,plex,1.0,51,16.5,not_published
1074 +Disraeli,31015,Chaudière-Appalaches,plex,1.0,51,15.8,not_published
1075 +Sainte-Sabine,28065,Chaudière-Appalaches,unifamilial,1.0,51,16.5,not_published
1076 +Saint-Basile-le-Grand,57020,Montérégie,plex,1.0,51,15.8,not_published
1077 +Mont-Royal,66072,Montréal,plex,1.0,51,15.8,not_published
1078 +Mercier,67045,Montérégie,condo,1.0,51,15.1,not_published
1079 +Clerval,87075,Abitibi-Témiscamingue,unifamilial,1.0,51,14.8,not_published
1080 +Saint-Edmond-de-Grantham,49100,Centre-du-Québec,unifamilial,1.0,51,16.2,not_published
1081 +Sainte-Angèle-de-Mérici,09035,Bas-Saint-Laurent,unifamilial,1.0,50,15.8,not_published
1082 +Bryson,84025,Outaouais,unifamilial,1.0,50,15.5,not_published
1083 +Notre-Dame-du-Sacré-Coeur-d'Issoudun,33085,Chaudière-Appalaches,unifamilial,1.0,50,15.5,not_published
1084 +Sainte-Françoise,11030,Bas-Saint-Laurent,unifamilial,1.0,50,14.4,not_published
1085 +Ulverton,42078,Estrie,unifamilial,1.0,50,16.2,not_published
1086 +Saint-Apollinaire,33090,Chaudière-Appalaches,plex,1.0,49,14.1,not_published
1087 +Sainte-Anne-des-Plaines,73035,Laurentides,condo,1.0,49,13.4,not_published
1088 +Saint-Eugène-de-Guigues,85085,Abitibi-Témiscamingue,unifamilial,1.0,49,15.5,not_published
1089 +Waltham,84070,Outaouais,unifamilial,1.0,49,14.8,not_published
1090 +Saint-Honoré-de-Témiscouata,13090,Bas-Saint-Laurent,unifamilial,1.0,49,15.1,not_published
1091 +La Motte,88045,Abitibi-Témiscamingue,unifamilial,1.0,49,16.2,not_published
1092 +Campbell's Bay,84030,Outaouais,unifamilial,1.0,49,15.5,not_published
1093 +Saint-François-de-Sales,91015,Saguenay–Lac-Saint-Jean,unifamilial,1.0,49,14.1,not_published
1094 +Lac-des-Seize-Îles,77055,Laurentides,unifamilial,1.0,49,14.1,not_published
1095 +Saint-Sulpice,60020,Lanaudière,condo,1.0,49,13.7,not_published
1096 +Irlande,31040,Chaudière-Appalaches,unifamilial,1.0,49,14.8,not_published
1097 +Scotstown,41080,Estrie,unifamilial,1.0,49,15.1,not_published
1098 +Saint-Télesphore,71015,Montérégie,unifamilial,1.0,49,14.8,not_published
1099 +Sainte-Anne-de-Bellevue,66117,Montréal,plex,1.0,49,14.4,not_published
1100 +La Visitation-de-l'Île-Dupas,52050,Lanaudière,unifamilial,1.0,49,16.2,not_published
1101 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,plex,1.0,49,15.1,not_published
1102 +Warwick,39077,Centre-du-Québec,plex,1.0,48,15.1,not_published
1103 +Pontiac,82030,Outaouais,plex,1.0,48,14.8,not_published
1104 +Sainte-Marie-de-Blandford,38015,Centre-du-Québec,unifamilial,1.0,48,14.8,not_published
1105 +Saint-Philippe-de-Néri,14060,Bas-Saint-Laurent,unifamilial,1.0,48,15.5,not_published
1106 +Lochaber-Partie-Ouest,80060,Outaouais,unifamilial,1.0,48,15.5,not_published
1107 +Murdochville,03025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,48,14.8,not_published
1108 +East Farnham,46085,Estrie,unifamilial,1.0,48,15.5,not_published
1109 +Saint-Roch-de-l'Achigan,63035,Lanaudière,plex,1.0,48,15.5,not_published
1110 +Shawinigan,36033,Mauricie,condo,1.0,48,14.4,not_published
1111 +Sainte-Brigitte-des-Saults,49085,Centre-du-Québec,unifamilial,1.0,48,15.5,not_published
1112 +Audet,30055,Estrie,unifamilial,1.0,48,15.5,not_published
1113 +Saint-Honoré,94240,Saguenay–Lac-Saint-Jean,plex,1.0,48,14.8,not_published
1114 +Beaconsfield,66107,Montréal,condo,1.0,47,15.1,not_published
1115 +Delson,67025,Montérégie,plex,1.0,47,14.4,not_published
1116 +Sainte-Paule,08040,Bas-Saint-Laurent,unifamilial,1.0,47,15.1,not_published
1117 +Coteau-du-Lac,71040,Montérégie,plex,1.0,47,14.4,not_published
1118 +Saint-Prosper-de-Champlain,37250,Mauricie,unifamilial,1.0,47,14.1,not_published
1119 +Stanstead,45008,Estrie,plex,1.0,47,15.1,not_published
1120 +Honfleur,19070,Chaudière-Appalaches,unifamilial,1.0,47,15.1,not_published
1121 +Sainte-Marie,26030,Chaudière-Appalaches,condo,1.0,47,15.5,not_published
1122 +Saint-Gabriel-Lalemant,14075,Bas-Saint-Laurent,unifamilial,1.0,47,16.2,not_published
1123 +Lawrenceville,42045,Estrie,unifamilial,1.0,47,15.1,not_published
1124 +Sainte-Séraphine,39105,Centre-du-Québec,unifamilial,1.0,47,14.4,not_published
1125 +Saint-Sylvère,38005,Centre-du-Québec,unifamilial,1.0,47,14.8,not_published
1126 +Sainte-Euphémie-sur-Rivière-du-Sud,18035,Chaudière-Appalaches,unifamilial,1.0,47,14.4,not_published
1127 +Notre-Dame-de-Lourdes,32080,Centre-du-Québec,unifamilial,1.0,47,14.8,not_published
1128 +Sainte-Louise,17060,Chaudière-Appalaches,unifamilial,1.0,46,13.7,not_published
1129 +Portneuf-sur-Mer,95040,Côte-Nord,unifamilial,1.0,46,14.1,not_published
1130 +Rivière-au-Tonnerre,98055,Côte-Nord,unifamilial,1.0,46,13.7,not_published
1131 +Bedford,46035,Estrie,plex,1.0,46,12.7,not_published
1132 +Senneville,66127,Montréal,unifamilial,1.0,46,13.7,not_published
1133 +Saint-Alphonse,05065,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,46,13.4,not_published
1134 +Saint-Samuel,39130,Centre-du-Québec,unifamilial,1.0,46,15.1,not_published
1135 +Tourville,17035,Chaudière-Appalaches,unifamilial,1.0,46,15.1,not_published
1136 +Saints-Martyrs-Canadiens,39005,Centre-du-Québec,unifamilial,1.0,46,14.1,not_published
1137 +Sainte-Martine,70012,Montérégie,plex,1.0,46,15.5,not_published
1138 +Les Coteaux,71033,Montérégie,condo,1.0,46,14.1,not_published
1139 +Saint-André-du-Lac-Saint-Jean,91010,Saguenay–Lac-Saint-Jean,unifamilial,1.0,45,12.7,not_published
1140 +Sainte-Perpétue,50050,Centre-du-Québec,unifamilial,1.0,45,12.7,not_published
1141 +Saint-Mathias-sur-Richelieu,55065,Montérégie,plex,1.0,45,14.4,not_published
1142 +Tadoussac,95005,Côte-Nord,unifamilial,1.0,45,14.1,not_published
1143 +Lac-Etchemin,28053,Chaudière-Appalaches,plex,1.0,45,14.8,not_published
1144 +Sainte-Apolline-de-Patton,18025,Chaudière-Appalaches,unifamilial,1.0,45,14.1,not_published
1145 +Saint-Lazare,71105,Montérégie,plex,1.0,45,13.7,not_published
1146 +Petit-Saguenay,94205,Saguenay–Lac-Saint-Jean,unifamilial,1.0,44,13.7,not_published
1147 +Saint-Marc-des-Carrières,34065,Capitale-Nationale,plex,1.0,44,13.7,not_published
1148 +Sainte-Anne-de-la-Rochelle,42050,Estrie,unifamilial,1.0,44,14.1,not_published
1149 +Ham-Sud,40005,Estrie,unifamilial,1.0,44,14.8,not_published
1150 +Baie-des-Sables,08080,Bas-Saint-Laurent,unifamilial,1.0,44,14.8,not_published
1151 +Saint-Malo,44003,Estrie,unifamilial,1.0,44,13.1,not_published
1152 +Les Cèdres,71050,Montérégie,condo,1.0,44,13.1,not_published
1153 +Saint-Adalbert,17015,Chaudière-Appalaches,unifamilial,1.0,44,14.1,not_published
1154 +Saint-Sébastien,56050,Montérégie,unifamilial,1.0,44,14.1,not_published
1155 +Otterburn Park,57030,Montérégie,plex,1.0,44,13.4,not_published
1156 +Candiac,67020,Montérégie,plex,1.0,43,13.4,not_published
1157 +Lac-Brome,46075,Estrie,plex,1.0,43,13.4,not_published
1158 +Verchères,59025,Montérégie,plex,1.0,43,13.4,not_published
1159 +Baie-Trinité,96005,Côte-Nord,unifamilial,1.0,43,13.4,not_published
1160 +Rawdon,62037,Lanaudière,condo,1.0,43,13.1,not_published
1161 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,plex,1.0,43,13.7,not_published
1162 +Saint-Joseph-du-Lac,72025,Laurentides,condo,1.0,43,13.1,not_published
1163 +Stornoway,30105,Estrie,unifamilial,1.0,43,14.4,not_published
1164 +Ormstown,69037,Montérégie,plex,1.0,43,13.1,not_published
1165 +Havre-Saint-Pierre,98040,Côte-Nord,plex,1.0,43,14.1,not_published
1166 +Saint-Augustin-de-Woburn,30005,Estrie,unifamilial,1.0,43,13.4,not_published
1167 +Biencourt,13055,Bas-Saint-Laurent,unifamilial,1.0,43,11.7,not_published
1168 +Huntingdon,69055,Montérégie,plex,1.0,43,13.4,not_published
1169 +Château-Richer,21035,Capitale-Nationale,plex,1.0,43,13.4,not_published
1170 +Senneterre,89040,Abitibi-Témiscamingue,plex,1.0,43,13.4,not_published
1171 +Sainte-Famille-de-l'Île-d'Orléans,20010,Capitale-Nationale,unifamilial,1.0,43,13.4,not_published
1172 +Portneuf,34048,Capitale-Nationale,plex,1.0,42,13.7,not_published
1173 +Saint-Luc-de-Vincennes,37225,Mauricie,unifamilial,1.0,42,14.1,not_published
1174 +Pike River,46025,Estrie,unifamilial,1.0,42,12.7,not_published
1175 +Rosemère,73020,Laurentides,plex,1.0,42,13.1,not_published
1176 +Saint-Jean-de-Matha,62015,Lanaudière,plex,1.0,42,12.4,not_published
1177 +Saint-Marcel,17020,Chaudière-Appalaches,unifamilial,1.0,42,12.7,not_published
1178 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,condo,1.0,42,13.1,not_published
1179 +Weedon,41098,Estrie,plex,1.0,42,14.1,not_published
1180 +Notre-Dame-de-Lourdes,61045,Lanaudière,plex,1.0,42,13.1,not_published
1181 +Saint-Paul-de-la-Croix,12035,Bas-Saint-Laurent,unifamilial,1.0,42,12.4,not_published
1182 +Saint-Adelme,08030,Bas-Saint-Laurent,unifamilial,1.0,41,13.1,not_published
1183 +Cowansville,46080,Estrie,condo,1.0,41,13.7,not_published
1184 +Saint-Adrien,40010,Estrie,unifamilial,1.0,41,13.1,not_published
1185 +Sainte-Gertrude-Manneville,88085,Abitibi-Témiscamingue,unifamilial,1.0,41,12.4,not_published
1186 +Saint-Joseph-de-Lepage,09070,Bas-Saint-Laurent,unifamilial,1.0,41,12.7,not_published
1187 +Brome,46070,Estrie,unifamilial,1.0,41,12.7,not_published
1188 +Roxton Pond,47047,Estrie,plex,1.0,41,12.4,not_published
1189 +Leclercville,33123,Chaudière-Appalaches,unifamilial,1.0,41,12.4,not_published
1190 +Montréal-Ouest,66047,Montréal,plex,1.0,41,12.7,not_published
1191 +Notre-Dame-de-Ham,39015,Centre-du-Québec,unifamilial,1.0,41,12.7,not_published
1192 +Forestville,95045,Côte-Nord,plex,1.0,41,13.1,not_published
1193 +Saint-Henri,19068,Chaudière-Appalaches,plex,1.0,41,13.1,not_published
1194 +Oka,72032,Laurentides,plex,1.0,40,12.0,not_published
1195 +Bonaventure,05045,Gaspésie–Îles-de-la-Madeleine,plex,1.0,40,12.7,not_published
1196 +Dundee,69075,Montérégie,unifamilial,1.0,40,12.4,not_published
1197 +Saint-Moïse,07095,Bas-Saint-Laurent,unifamilial,1.0,40,12.7,not_published
1198 +Hampstead,66062,Montréal,plex,1.0,40,13.7,not_published
1199 +Boischatel,21045,Capitale-Nationale,plex,1.0,40,11.7,not_published
1200 +Saint-Sixte,80070,Outaouais,unifamilial,1.0,40,12.4,not_published
1201 +Saint-Simon-les-Mines,29125,Chaudière-Appalaches,unifamilial,1.0,40,13.4,not_published
1202 +Saint-Adrien-d'Irlande,31095,Chaudière-Appalaches,unifamilial,1.0,39,12.0,not_published
1203 +Saint-Camille,40025,Estrie,unifamilial,1.0,39,12.7,not_published
1204 +Barnston-Ouest,44045,Estrie,unifamilial,1.0,39,12.7,not_published
1205 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,plex,1.0,39,12.7,not_published
1206 +Bedford,46040,Estrie,unifamilial,1.0,39,12.0,not_published
1207 +Saint-Joseph-de-Beauce,27043,Chaudière-Appalaches,plex,1.0,39,12.0,not_published
1208 +Saint-Louis-de-Gonzague,28035,Chaudière-Appalaches,unifamilial,1.0,39,13.1,not_published
1209 +Villeroy,32085,Centre-du-Québec,unifamilial,1.0,38,11.0,not_published
1210 +Grenville,76055,Laurentides,plex,1.0,38,12.4,not_published
1211 +Roquemaure,87015,Abitibi-Témiscamingue,unifamilial,1.0,38,12.4,not_published
1212 +Papineauville,80037,Outaouais,plex,1.0,38,11.7,not_published
1213 +Normétal,87115,Abitibi-Témiscamingue,unifamilial,1.0,38,12.4,not_published
1214 +Rivière-Éternité,94215,Saguenay–Lac-Saint-Jean,unifamilial,1.0,38,12.0,not_published
1215 +Saint-François-de-l'Île-d'Orléans,20005,Capitale-Nationale,unifamilial,1.0,38,12.0,not_published
1216 +Saint-Côme,62065,Lanaudière,plex,1.0,38,12.0,not_published
1217 +Lingwick,41085,Estrie,unifamilial,1.0,38,12.0,not_published
1218 +Saint-Germain-de-Grantham,49048,Centre-du-Québec,plex,1.0,38,12.0,not_published
1219 +Saint-Lazare,71105,Montérégie,condo,1.0,38,10.7,not_published
1220 +Sainte-Thècle,35050,Mauricie,plex,1.0,38,12.4,not_published
1221 +Dégelis,13005,Bas-Saint-Laurent,plex,1.0,38,11.7,not_published
1222 +Stanstead-Est,44050,Estrie,unifamilial,1.0,38,13.1,not_published
1223 +Cascapédia–Saint-Jules,05077,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,38,12.4,not_published
1224 +Abercorn,46005,Estrie,unifamilial,1.0,37,11.0,not_published
1225 +Saint-Denis-de-Brompton,42025,Estrie,plex,1.0,37,11.7,not_published
1226 +Fortierville,38047,Centre-du-Québec,unifamilial,1.0,37,11.3,not_published
1227 +Saint-Ambroise-de-Kildare,61040,Lanaudière,plex,1.0,37,11.0,not_published
1228 +Kinnear's Mills,31105,Chaudière-Appalaches,unifamilial,1.0,37,11.7,not_published
1229 +Martinville,44060,Estrie,unifamilial,1.0,37,12.0,not_published
1230 +Saint-Célestin,50035,Centre-du-Québec,unifamilial,1.0,37,11.3,not_published
1231 +Val-d'Or,89008,Abitibi-Témiscamingue,condo,1.0,37,11.7,not_published
1232 +Saint-Valentin,56030,Montérégie,unifamilial,1.0,37,11.7,not_published
1233 +Saint-Philippe,67010,Montérégie,plex,1.0,37,12.4,not_published
1234 +Pohénégamook,13095,Bas-Saint-Laurent,plex,1.0,37,12.0,not_published
1235 +Neuville,34007,Capitale-Nationale,plex,1.0,36,10.7,not_published
1236 +Notre-Dame-de-Bonsecours,80015,Outaouais,unifamilial,1.0,36,10.7,not_published
1237 +Saint-Félix-de-Valois,62007,Lanaudière,condo,1.0,36,11.7,not_published
1238 +Alma,93042,Saguenay–Lac-Saint-Jean,condo,1.0,36,11.7,not_published
1239 +Poularies,87035,Abitibi-Témiscamingue,unifamilial,1.0,36,12.0,not_published
1240 +Rémigny,85105,Abitibi-Témiscamingue,unifamilial,1.0,36,12.0,not_published
1241 +Latulipe-et-Gaboury,85060,Abitibi-Témiscamingue,unifamilial,1.0,36,11.7,not_published
1242 +Saint-François-d'Assise,06055,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,36,11.0,not_published
1243 +Saint-Marc-du-Lac-Long,13020,Bas-Saint-Laurent,unifamilial,1.0,36,11.7,not_published
1244 +Saint-Alfred,27015,Chaudière-Appalaches,unifamilial,1.0,35,11.7,not_published
1245 +Saint-Marcel-de-Richelieu,54125,Montérégie,unifamilial,1.0,35,10.3,not_published
1246 +Nédélec,85100,Abitibi-Témiscamingue,unifamilial,1.0,35,11.3,not_published
1247 +Notre-Dame-de-Stanbridge,46100,Estrie,unifamilial,1.0,35,12.0,not_published
1248 +Saint-Thomas,61027,Lanaudière,plex,1.0,35,10.3,not_published
1249 +Sainte-Hélène-de-Mancebourg,87070,Abitibi-Témiscamingue,unifamilial,1.0,35,10.3,not_published
1250 +Saint-Cyrille-de-Wendover,49070,Centre-du-Québec,plex,1.0,35,10.3,not_published
1251 +Kamouraska,14050,Bas-Saint-Laurent,unifamilial,1.0,35,11.7,not_published
1252 +Valcourt,42055,Estrie,plex,1.0,35,11.3,not_published
1253 +Dorval,66087,Montréal,plex,1.0,35,10.7,not_published
1254 +Saint-Thuribe,34085,Capitale-Nationale,unifamilial,1.0,35,10.3,not_published
1255 +Gracefield,83032,Outaouais,plex,1.0,34,11.0,not_published
1256 +Matapédia,06045,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,34,11.3,not_published
1257 +La Conception,78115,Laurentides,plex,1.0,34,11.3,not_published
1258 +Milan,30040,Estrie,unifamilial,1.0,34,11.0,not_published
1259 +Sainte-Madeleine,54025,Montérégie,plex,1.0,34,11.0,not_published
1260 +Saint-Stanislas,92070,Saguenay–Lac-Saint-Jean,unifamilial,1.0,34,11.0,not_published
1261 +Terrasse-Vaudreuil,71075,Montérégie,plex,1.0,34,11.0,not_published
1262 +Saint-Bruno-de-Kamouraska,14010,Bas-Saint-Laurent,unifamilial,1.0,34,11.3,not_published
1263 +Lebel-sur-Quévillon,99005,Nord-du-Québec,plex,1.0,33,11.0,not_published
1264 +Saint-Clément,11005,Bas-Saint-Laurent,unifamilial,1.0,33,10.3,not_published
1265 +Saint-Octave-de-Métis,09055,Bas-Saint-Laurent,unifamilial,1.0,33,11.0,not_published
1266 +Saint-Damase-de-L'Islet,17040,Chaudière-Appalaches,unifamilial,1.0,33,10.0,not_published
1267 +Sainte-Anne-de-la-Pérade,37205,Mauricie,plex,1.0,33,10.3,not_published
1268 +Lac-des-Aigles,13062,Bas-Saint-Laurent,unifamilial,1.0,33,9.3,not_published
1269 +Les Hauteurs,09015,Bas-Saint-Laurent,unifamilial,1.0,33,11.0,not_published
1270 +Escuminac,06025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,33,11.0,not_published
1271 +Sainte-Agathe-des-Monts,78032,Laurentides,condo,1.0,32,10.3,not_published
1272 +Saint-Adolphe-d'Howard,77065,Laurentides,plex,1.0,32,10.7,not_published
1273 +Saint-Omer,17005,Chaudière-Appalaches,unifamilial,1.0,32,10.0,not_published
1274 +Chichester,84090,Outaouais,unifamilial,1.0,32,10.0,not_published
1275 +Lochaber,80055,Outaouais,unifamilial,1.0,32,10.3,not_published
1276 +Saint-André-de-Kamouraska,14040,Bas-Saint-Laurent,unifamilial,1.0,32,10.0,not_published
1277 +Sainte-Irène,07040,Bas-Saint-Laurent,unifamilial,1.0,32,11.0,not_published
1278 +Notre-Dame-des-Monts,15025,Capitale-Nationale,unifamilial,1.0,32,10.0,not_published
1279 +Colombier,95050,Côte-Nord,unifamilial,1.0,32,10.3,not_published
1280 +Berry,88070,Abitibi-Témiscamingue,unifamilial,1.0,32,10.7,not_published
1281 +Maricourt,42065,Estrie,unifamilial,1.0,31,10.0,not_published
1282 +Moffet,85075,Abitibi-Témiscamingue,unifamilial,1.0,31,10.7,not_published
1283 +Egan-Sud,83075,Outaouais,unifamilial,1.0,31,10.7,not_published
1284 +Saint-Bernard-de-Michaudville,54115,Montérégie,unifamilial,1.0,31,10.3,not_published
1285 +Franquelin,96015,Côte-Nord,unifamilial,1.0,31,10.3,not_published
1286 +Victoriaville,39062,Centre-du-Québec,condo,1.0,31,10.0,not_published
1287 +Saint-Jean-de-la-Lande,13010,Bas-Saint-Laurent,unifamilial,1.0,31,10.7,not_published
1288 +Kipawa,85010,Abitibi-Témiscamingue,unifamilial,1.0,31,9.6,not_published
1289 +Saint-Elzéar,05050,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,31,10.0,not_published
1290 +Macamic,87058,Abitibi-Témiscamingue,plex,1.0,31,9.6,not_published
1291 +Lac-Supérieur,78095,Laurentides,plex,1.0,31,10.0,not_published
1292 +Grand-Métis,09060,Bas-Saint-Laurent,unifamilial,1.0,31,10.7,not_published
1293 +Saint-Dominique-du-Rosaire,88065,Abitibi-Témiscamingue,unifamilial,1.0,30,9.6,not_published
1294 +Chartierville,41020,Estrie,unifamilial,1.0,30,9.6,not_published
1295 +Saint-Roch-de-Richelieu,53040,Montérégie,plex,1.0,30,9.6,not_published
1296 +Saint-Boniface,51085,Mauricie,plex,1.0,30,10.0,not_published
1297 +Val-Racine,30015,Estrie,unifamilial,1.0,30,8.6,not_published
1298 +Saint-Jules,27055,Chaudière-Appalaches,unifamilial,1.0,30,10.3,not_published
1299 +Maddington Falls,39165,Centre-du-Québec,unifamilial,1.0,30,10.0,not_published
1300 +Lacolle,56023,Montérégie,plex,1.0,30,9.6,not_published
1301 +La Trinité-des-Monts,10010,Bas-Saint-Laurent,unifamilial,1.0,30,10.3,not_published
1302 +Saint-Jean-Baptiste,57033,Montérégie,plex,1.0,30,9.3,not_published
1303 +Saint-Vianney,07075,Bas-Saint-Laurent,unifamilial,1.0,30,8.9,not_published
1304 +Clermont,87110,Abitibi-Témiscamingue,unifamilial,1.0,30,10.3,not_published
1305 +Sainte-Thérèse-de-Gaspé,02010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,29,9.3,not_published
1306 +Ferme-Neuve,79097,Laurentides,plex,1.0,29,10.0,not_published
1307 +Saint-Paul,61005,Lanaudière,condo,1.0,29,9.3,not_published
1308 +Saint-François-Xavier-de-Viger,12025,Bas-Saint-Laurent,unifamilial,1.0,29,9.3,not_published
1309 +Saint-Anselme,19062,Chaudière-Appalaches,plex,1.0,29,9.3,not_published
1310 +Guérin,85095,Abitibi-Témiscamingue,unifamilial,1.0,29,9.3,not_published
1311 +Saint-Robert-Bellarmin,30070,Estrie,unifamilial,1.0,29,9.3,not_published
1312 +Saint-Ignace-de-Stanbridge,46095,Estrie,unifamilial,1.0,29,9.3,not_published
1313 +Elgin,69050,Montérégie,unifamilial,1.0,29,9.6,not_published
1314 +Sainte-Sophie-d'Halifax,32023,Centre-du-Québec,unifamilial,1.0,29,9.6,not_published
1315 +Saint-Sulpice,60020,Lanaudière,plex,1.0,29,9.6,not_published
1316 +Port-Cartier,97022,Côte-Nord,plex,1.0,29,8.6,not_published
1317 +Ascot Corner,41055,Estrie,plex,1.0,28,8.9,not_published
1318 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,plex,1.0,28,8.6,not_published
1319 +Saint-Eugène-d'Argentenay,92065,Saguenay–Lac-Saint-Jean,unifamilial,1.0,28,8.9,not_published
1320 +Sainte-Mélanie,61050,Lanaudière,plex,1.0,28,9.3,not_published
1321 +La Rédemption,09005,Bas-Saint-Laurent,unifamilial,1.0,28,9.3,not_published
1322 +Saint-Nazaire-de-Dorchester,19015,Chaudière-Appalaches,unifamilial,1.0,28,8.9,not_published
1323 +Barkmere,78050,Laurentides,unifamilial,1.0,28,9.3,not_published
1324 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,plex,1.0,28,8.2,not_published
1325 +Blanc-Sablon,98005,Côte-Nord,unifamilial,1.0,28,8.9,not_published
1326 +Warden,47030,Estrie,unifamilial,1.0,28,9.3,not_published
1327 +Saint-Eusèbe,13030,Bas-Saint-Laurent,unifamilial,1.0,28,8.9,not_published
1328 +Saint-Agapit,33045,Chaudière-Appalaches,plex,1.0,27,8.9,not_published
1329 +Saint-François-du-Lac,50128,Centre-du-Québec,plex,1.0,27,7.2,not_published
1330 +Bois-Franc,83085,Outaouais,unifamilial,1.0,27,9.3,not_published
1331 +Saint-Pie-de-Guire,49130,Centre-du-Québec,unifamilial,1.0,27,9.3,not_published
1332 +Sainte-Madeleine-de-la-Rivière-Madeleine,04005,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,27,9.3,not_published
1333 +Saint-Alexis-de-Matapédia,06050,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,27,9.3,not_published
1334 +Saint-Ambroise,94255,Saguenay–Lac-Saint-Jean,plex,1.0,27,9.3,not_published
1335 +Notre-Dame-du-Mont-Carmel,37235,Mauricie,plex,1.0,27,8.9,not_published
1336 +Chelsea,82025,Outaouais,plex,1.0,27,8.2,not_published
1337 +Maskinongé,51008,Mauricie,plex,1.0,27,8.9,not_published
1338 +Sainte-Edwidge-de-Clifton,44055,Estrie,unifamilial,1.0,27,9.3,not_published
1339 +Saint-Alphonse-Rodriguez,62025,Lanaudière,plex,1.0,27,8.9,not_published
1340 +Lac-des-Écorces,79078,Laurentides,plex,1.0,27,8.2,not_published
1341 +Lac-Frontière,18010,Chaudière-Appalaches,unifamilial,1.0,27,9.3,not_published
1342 +Saint-Bruno-de-Montarville,58037,Montérégie,plex,1.0,27,9.3,not_published
1343 +Laurier-Station,33060,Chaudière-Appalaches,plex,1.0,27,8.9,not_published
1344 +Saint-Augustin,92005,Saguenay–Lac-Saint-Jean,unifamilial,1.0,27,8.9,not_published
1345 +Godbout,96010,Côte-Nord,unifamilial,1.0,27,9.3,not_published
1346 +Saint-Éloi,11035,Bas-Saint-Laurent,unifamilial,1.0,27,8.9,not_published
1347 +Esprit-Saint,10005,Bas-Saint-Laurent,unifamilial,1.0,27,8.9,not_published
1348 +Saint-Prosper,28020,Chaudière-Appalaches,plex,1.0,27,8.9,not_published
1349 +Daveluyville,39152,Centre-du-Québec,plex,1.0,26,8.6,not_published
1350 +Sainte-Luce,09092,Bas-Saint-Laurent,plex,1.0,26,8.6,not_published
1351 +Otterburn Park,57030,Montérégie,condo,1.0,26,7.6,not_published
1352 +Saint-Alexis-des-Monts,51065,Mauricie,plex,1.0,26,7.9,not_published
1353 +Waterville,44080,Estrie,plex,1.0,26,8.2,not_published
1354 +Longue-Pointe-de-Mingan,98045,Côte-Nord,unifamilial,1.0,26,8.2,not_published
1355 +Barraute,88022,Abitibi-Témiscamingue,plex,1.0,26,7.9,not_published
1356 +Saint-Aimé,53015,Montérégie,unifamilial,1.0,26,8.2,not_published
1357 +Sainte-Claire,19055,Chaudière-Appalaches,plex,1.0,26,8.6,not_published
1358 +Sainte-Félicité,17025,Chaudière-Appalaches,unifamilial,1.0,26,7.6,not_published
1359 +Portage-du-Fort,84020,Outaouais,unifamilial,1.0,26,7.9,not_published
1360 +Sainte-Croix,33102,Chaudière-Appalaches,plex,1.0,26,7.6,not_published
1361 +Saint-Casimir,34078,Capitale-Nationale,plex,1.0,26,7.6,not_published
1362 +Saint-Zénon-du-Lac-Humqui,07035,Bas-Saint-Laurent,unifamilial,1.0,26,7.9,not_published
1363 +East Broughton,31122,Chaudière-Appalaches,plex,1.0,26,8.6,not_published
1364 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,plex,1.0,26,8.6,not_published
1365 +Saint-Antoine-de-l'Isle-aux-Grues,18070,Chaudière-Appalaches,unifamilial,1.0,26,7.9,not_published
1366 +Saint-Nazaire,93045,Saguenay–Lac-Saint-Jean,plex,1.0,26,8.9,not_published
1367 +Yamachiche,51020,Mauricie,plex,1.0,26,8.6,not_published
1368 +Saint-Julien,31035,Chaudière-Appalaches,unifamilial,1.0,25,8.2,not_published
1369 +Saint-Séverin,27070,Chaudière-Appalaches,unifamilial,1.0,25,8.2,not_published
1370 +McMasterville,57025,Montérégie,condo,1.0,25,8.2,not_published
1371 +Hope,05025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,25,8.6,not_published
1372 +Cap-Saint-Ignace,18045,Chaudière-Appalaches,plex,1.0,25,8.2,not_published
1373 +Saint-Noël,07100,Bas-Saint-Laurent,unifamilial,1.0,25,8.2,not_published
1374 +La Pêche,82035,Outaouais,condo,1.0,25,8.2,not_published
1375 +Saint-Esprit,63030,Lanaudière,plex,1.0,25,7.9,not_published
1376 +Calixa-Lavallée,59030,Montérégie,unifamilial,1.0,25,8.6,not_published
1377 +L'Islet,17078,Chaudière-Appalaches,plex,1.0,25,7.9,not_published
1378 +Saint-Léandre,08065,Bas-Saint-Laurent,unifamilial,1.0,25,8.2,not_published
1379 +Orford,45115,Estrie,plex,1.0,25,8.2,not_published
1380 +Deschambault-Grondines,34058,Capitale-Nationale,plex,1.0,24,8.2,not_published
1381 +Lyster,32065,Centre-du-Québec,plex,1.0,24,7.2,not_published
1382 +Chazel,87095,Abitibi-Témiscamingue,unifamilial,1.0,24,7.2,not_published
1383 +Sainte-Monique,50057,Centre-du-Québec,unifamilial,1.0,24,8.2,not_published
1384 +Sainte-Hélène-de-Chester,39035,Centre-du-Québec,unifamilial,1.0,24,7.9,not_published
1385 +Ange-Gardien,55008,Montérégie,plex,1.0,24,7.6,not_published
1386 +Yamaska,53072,Montérégie,plex,1.0,24,7.6,not_published
1387 +Ivry-sur-le-Lac,78042,Laurentides,unifamilial,1.0,24,8.2,not_published
1388 +Saint-Tharcisius,07070,Bas-Saint-Laurent,unifamilial,1.0,24,7.9,not_published
1389 +Pointe-Claire,66097,Montréal,plex,1.0,24,8.2,not_published
1390 +Mont-Saint-Grégoire,56097,Montérégie,plex,1.0,24,7.2,not_published
1391 +Potton,45030,Estrie,condo,1.0,24,7.9,not_published
1392 +New Richmond,05070,Gaspésie–Îles-de-la-Madeleine,plex,1.0,24,7.9,not_published
1393 +Aston-Jonction,50013,Centre-du-Québec,unifamilial,1.0,24,7.9,not_published
1394 +Authier-Nord,87100,Abitibi-Témiscamingue,unifamilial,1.0,23,7.9,not_published
1395 +Saint-Jean-Port-Joli,17070,Chaudière-Appalaches,plex,1.0,23,7.6,not_published
1396 +Normandin,92040,Saguenay–Lac-Saint-Jean,plex,1.0,23,7.9,not_published
1397 +Sainte-Lucie-de-Beauregard,18020,Chaudière-Appalaches,unifamilial,1.0,23,7.9,not_published
1398 +Saint-Henri,19068,Chaudière-Appalaches,condo,1.0,23,6.5,not_published
1399 +Saint-Gabriel-de-Valcartier,22025,Capitale-Nationale,plex,1.0,23,7.6,not_published
1400 +Saint-Sévère,51030,Mauricie,unifamilial,1.0,23,7.6,not_published
1401 +Sainte-Françoise,38035,Centre-du-Québec,unifamilial,1.0,23,7.9,not_published
1402 +Saint-Antonin,12015,Bas-Saint-Laurent,plex,1.0,23,7.2,not_published
1403 +Notre-Dame-du-Laus,79005,Laurentides,plex,1.0,23,7.6,not_published
1404 +Sainte-Florence,07010,Bas-Saint-Laurent,unifamilial,1.0,23,6.9,not_published
1405 +Sainte-Marie-Madeleine,54030,Montérégie,plex,1.0,23,7.2,not_published
1406 +Saint-André-d'Argenteuil,76008,Laurentides,plex,1.0,23,7.6,not_published
1407 +Grand-Saint-Esprit,50065,Centre-du-Québec,unifamilial,1.0,23,7.6,not_published
1408 +Sainte-Clotilde-de-Beauce,31060,Chaudière-Appalaches,unifamilial,1.0,23,7.9,not_published
1409 +Sacré-Coeur-de-Jésus,31130,Chaudière-Appalaches,unifamilial,1.0,23,7.6,not_published
1410 +Saint-Paulin,51060,Mauricie,plex,1.0,23,6.9,not_published
1411 +Saint-Pierre-de-Lamy,13075,Bas-Saint-Laurent,unifamilial,1.0,23,7.6,not_published
1412 +Saint-Eugène-de-Ladrière,10075,Bas-Saint-Laurent,unifamilial,1.0,23,7.9,not_published
1413 +Béthanie,48005,Montérégie,unifamilial,1.0,23,7.9,not_published
1414 +Saint-Barthélemy,52055,Lanaudière,plex,1.0,23,7.9,not_published
1415 +Saint-Anaclet-de-Lessard,10030,Bas-Saint-Laurent,plex,1.0,23,7.6,not_published
1416 +Eastman,45093,Estrie,plex,1.0,22,7.6,not_published
1417 +Sainte-Élisabeth,52030,Lanaudière,plex,1.0,22,7.6,not_published
1418 +Sainte-Cécile-de-Lévrard,38060,Centre-du-Québec,unifamilial,1.0,22,7.6,not_published
1419 +Rapide-Danseur,87010,Abitibi-Témiscamingue,unifamilial,1.0,22,7.6,not_published
1420 +Saint-Apollinaire,33090,Chaudière-Appalaches,condo,1.0,22,6.9,not_published
1421 +Les Méchins,08005,Bas-Saint-Laurent,unifamilial,1.0,22,6.5,not_published
1422 +Wickham,49040,Centre-du-Québec,plex,1.0,22,6.9,not_published
1423 +Sainte-Justine,28045,Chaudière-Appalaches,plex,1.0,22,7.6,not_published
1424 +Roxton Falls,48010,Montérégie,plex,1.0,22,7.6,not_published
1425 +Beauharnois,70022,Montérégie,condo,1.0,22,6.9,not_published
1426 +Marsoui,04025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,22,7.2,not_published
1427 +La Présentation,54035,Montérégie,plex,1.0,22,7.2,not_published
1428 +Rivière-du-Loup,12072,Bas-Saint-Laurent,condo,1.0,22,6.5,not_published
1429 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,plex,1.0,22,7.6,not_published
1430 +Dollard-des-Ormeaux,66142,Montréal,plex,1.0,22,7.6,not_published
1431 +East Hereford,44010,Estrie,unifamilial,1.0,22,6.9,not_published
1432 +Chénéville,80103,Outaouais,plex,1.0,21,6.2,not_published
1433 +Napierville,68030,Montérégie,plex,1.0,21,7.2,not_published
1434 +Saint-Martin,29045,Chaudière-Appalaches,plex,1.0,21,6.5,not_published
1435 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,plex,1.0,21,6.5,not_published
1436 +Fugèreville,85055,Abitibi-Témiscamingue,unifamilial,1.0,21,6.9,not_published
1437 +Rougemont,55037,Montérégie,plex,1.0,21,7.2,not_published
1438 +Saint-Lin–Laurentides,63048,Lanaudière,condo,1.0,21,6.5,not_published
1439 +Saint-Jean-de-Brébeuf,31100,Chaudière-Appalaches,unifamilial,1.0,21,7.2,not_published
1440 +Saint-Alexandre-de-Kamouraska,14035,Bas-Saint-Laurent,plex,1.0,21,6.9,not_published
1441 +Amherst,78070,Laurentides,plex,1.0,21,6.5,not_published
1442 +Sainte-Catherine,67030,Montérégie,condo,1.0,21,6.9,not_published
1443 +Rigaud,71133,Montérégie,condo,1.0,21,6.9,not_published
1444 +Sainte-Barbe,69065,Montérégie,plex,1.0,21,7.2,not_published
1445 +Cap-Santé,34030,Capitale-Nationale,plex,1.0,21,6.9,not_published
1446 +Saint-Raphaël,19082,Chaudière-Appalaches,plex,1.0,21,7.2,not_published
1447 +Stoke,42005,Estrie,plex,1.0,21,6.5,not_published
1448 +Lac-Tremblant-Nord,78127,Laurentides,unifamilial,1.0,21,7.2,not_published
1449 +Padoue,09040,Bas-Saint-Laurent,unifamilial,1.0,21,7.2,not_published
1450 +Sainte-Élizabeth-de-Warwick,39090,Centre-du-Québec,unifamilial,1.0,21,6.5,not_published
1451 +Sainte-Rita,11015,Bas-Saint-Laurent,unifamilial,1.0,21,6.5,not_published
1452 +Témiscaming,85005,Abitibi-Témiscamingue,plex,1.0,21,6.9,not_published
1453 +La Reine,87080,Abitibi-Témiscamingue,unifamilial,1.0,21,6.9,not_published
1454 +Saint-Fortunat,31030,Chaudière-Appalaches,unifamilial,1.0,21,6.5,not_published
1455 +Saint-Prime,91035,Saguenay–Lac-Saint-Jean,plex,1.0,21,6.9,not_published
1456 +Lavaltrie,52007,Lanaudière,condo,1.0,20,6.5,not_published
1457 +Upton,48038,Montérégie,plex,1.0,20,6.2,not_published
1458 +Lac-Normand,35904,Mauricie,unifamilial,1.0,20,6.9,not_published
1459 +Saint-Fulgence,94235,Saguenay–Lac-Saint-Jean,plex,1.0,20,6.9,not_published
1460 +Saint-Basile,34038,Capitale-Nationale,plex,1.0,20,6.9,not_published
1461 +Saint-Charles-de-Bellechasse,19097,Chaudière-Appalaches,plex,1.0,20,6.2,not_published
1462 +Baie-Sainte-Catherine,15065,Capitale-Nationale,unifamilial,1.0,20,6.5,not_published
1463 +Ristigouche-Sud-Est,06035,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,20,5.5,not_published
1464 +Sainte-Victoire-de-Sorel,53025,Montérégie,plex,1.0,20,6.9,not_published
1465 +Sainte-Jeanne-d'Arc-de-la-Mitis,09020,Bas-Saint-Laurent,unifamilial,1.0,20,6.9,not_published
1466 +Laniel,85905,Abitibi-Témiscamingue,unifamilial,1.0,20,6.5,not_published
1467 +Saint-Maurice,37230,Mauricie,plex,1.0,20,6.2,not_published
1468 +Lac-aux-Sables,35010,Mauricie,plex,1.0,20,6.2,not_published
1469 +Saint-Joachim,21020,Capitale-Nationale,plex,1.0,20,6.5,not_published
1470 +Caplan,05060,Gaspésie–Îles-de-la-Madeleine,plex,1.0,20,6.5,not_published
1471 +Les Cèdres,71050,Montérégie,plex,1.0,20,6.2,not_published
1472 +Saint-Tite-des-Caps,21005,Capitale-Nationale,plex,1.0,20,6.9,not_published
1473 +Saint-Gérard-Majella,53085,Montérégie,unifamilial,1.0,20,6.5,not_published
1474 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,plex,1.0,20,5.8,not_published
1475 +Cacouna,12057,Bas-Saint-Laurent,plex,1.0,20,6.9,not_published
1476 +Saint-Lambert,87120,Abitibi-Témiscamingue,unifamilial,1.0,19,6.2,not_published
1477 +Grosse-Île,01042,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,19,5.8,not_published
1478 +Pointe-des-Cascades,71055,Montérégie,plex,1.0,19,6.2,not_published
1479 +Saint-Pascal,14018,Bas-Saint-Laurent,plex,1.0,19,6.2,not_published
1480 +Petite-Rivière-Saint-François,16005,Capitale-Nationale,plex,1.0,19,6.2,not_published
1481 +Saint-Placide,72043,Laurentides,plex,1.0,19,6.2,not_published
1482 +Mandeville,52095,Lanaudière,plex,1.0,19,6.5,not_published
1483 +Hope Town,05020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,19,6.5,not_published
1484 +Saint-Côme–Linière,29057,Chaudière-Appalaches,plex,1.0,19,6.2,not_published
1485 +Desbiens,93005,Saguenay–Lac-Saint-Jean,plex,1.0,19,6.5,not_published
1486 +La Visitation-de-Yamaska,50085,Centre-du-Québec,unifamilial,1.0,19,6.2,not_published
1487 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,condo,1.0,19,5.8,not_published
1488 +Grenville-sur-la-Rouge,76052,Laurentides,plex,1.0,19,6.5,not_published
1489 +Saint-Jacques-le-Majeur-de-Wolfestown,31025,Chaudière-Appalaches,unifamilial,1.0,19,6.2,not_published
1490 +Cacouna,12057,Bas-Saint-Laurent,condo,1.0,19,6.2,not_published
1491 +Saint-Siméon,05055,Gaspésie–Îles-de-la-Madeleine,plex,1.0,19,6.5,not_published
1492 +Rivière-aux-Outardes,96902,Côte-Nord,unifamilial,1.0,19,5.8,not_published
1493 +Sainte-Catherine-de-Hatley,45060,Estrie,plex,1.0,19,6.5,not_published
1494 +Saint-Bruno-de-Guigues,85045,Abitibi-Témiscamingue,plex,1.0,19,6.5,not_published
1495 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,plex,1.0,19,6.5,not_published
1496 +Rivière-Beaudette,71005,Montérégie,plex,1.0,19,6.5,not_published
1497 +Saint-Dominique,54060,Montérégie,plex,1.0,18,5.8,not_published
1498 +Shawville,84010,Outaouais,plex,1.0,18,5.8,not_published
1499 +La Guadeloupe,29030,Chaudière-Appalaches,plex,1.0,18,6.2,not_published
1500 +Saint-Siméon,15058,Capitale-Nationale,plex,1.0,18,5.8,not_published
1501 +Deux-Montagnes,72010,Laurentides,condo,1.0,18,5.5,not_published
1502 +Lorraine,73025,Laurentides,condo,1.0,18,6.2,not_published
1503 +Pincourt,71070,Montérégie,plex,1.0,18,5.5,not_published
1504 +Saint-Paul-d'Abbotsford,55015,Montérégie,plex,1.0,18,5.8,not_published
1505 +Sainte-Marcelline-de-Kildare,62030,Lanaudière,plex,1.0,18,5.8,not_published
1506 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,plex,1.0,18,5.5,not_published
1507 +Saint-Joseph-de-Kamouraska,14030,Bas-Saint-Laurent,unifamilial,1.0,18,6.2,not_published
1508 +Dunham,46050,Estrie,plex,1.0,18,6.2,not_published
1509 +Saint-Ignace-de-Loyola,52045,Lanaudière,plex,1.0,18,6.2,not_published
1510 +Venise-en-Québec,56005,Montérégie,plex,1.0,18,6.2,not_published
1511 +Albertville,07025,Bas-Saint-Laurent,unifamilial,1.0,18,6.2,not_published
1512 +Saint-Isidore,67040,Montérégie,plex,1.0,18,5.2,not_published
1513 +La Martre,04030,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,18,5.8,not_published
1514 +Saint-Athanase,13100,Bas-Saint-Laurent,unifamilial,1.0,18,5.8,not_published
1515 +Chambord,91020,Saguenay–Lac-Saint-Jean,plex,1.0,18,5.2,not_published
1516 +Mansfield-et-Pontefract,84065,Outaouais,plex,1.0,18,6.2,not_published
1517 +Saint-Éphrem-de-Beauce,29112,Chaudière-Appalaches,plex,1.0,18,5.8,not_published
1518 +Hampden,41075,Estrie,unifamilial,1.0,18,5.8,not_published
1519 +Pierreville,50113,Centre-du-Québec,plex,1.0,18,5.8,not_published
1520 +Saint-Edmond-les-Plaines,92050,Saguenay–Lac-Saint-Jean,unifamilial,1.0,18,6.2,not_published
1521 +Saint-Narcisse,37240,Mauricie,plex,1.0,18,6.2,not_published
1522 +Saint-Gédéon-de-Beauce,29013,Chaudière-Appalaches,plex,1.0,18,5.5,not_published
1523 +Saint-Cléophas-de-Brandon,52075,Lanaudière,unifamilial,1.0,18,5.5,not_published
1524 +La Malbaie,15013,Capitale-Nationale,condo,1.0,18,5.5,not_published
1525 +Saint-Fabien,10070,Bas-Saint-Laurent,plex,1.0,18,5.5,not_published
1526 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,condo,1.0,18,5.8,not_published
1527 +Saint-Gilbert,34060,Capitale-Nationale,unifamilial,1.0,18,6.2,not_published
1528 +Grande-Rivière,02015,Gaspésie–Îles-de-la-Madeleine,plex,1.0,18,5.8,not_published
1529 +Val-des-Lacs,78100,Laurentides,plex,1.0,17,5.2,not_published
1530 +Saint-Jean-de-Dieu,11010,Bas-Saint-Laurent,plex,1.0,17,5.8,not_published
1531 +Saint-François-Xavier-de-Brompton,42020,Estrie,plex,1.0,17,5.2,not_published
1532 +Sept-Îles,97007,Côte-Nord,condo,1.0,17,5.2,not_published
1533 +Sainte-Anne-de-Sorel,53065,Montérégie,plex,1.0,17,5.5,not_published
1534 +Lorrainville,85037,Abitibi-Témiscamingue,plex,1.0,17,5.8,not_published
1535 +Larouche,94265,Saguenay–Lac-Saint-Jean,plex,1.0,17,5.8,not_published
1536 +Champlain,37220,Mauricie,plex,1.0,17,5.2,not_published
1537 +Saint-Elzéar-de-Témiscouata,13085,Bas-Saint-Laurent,unifamilial,1.0,17,5.8,not_published
1538 +Montebello,80010,Outaouais,plex,1.0,17,5.5,not_published
1539 +Saint-Polycarpe,71020,Montérégie,plex,1.0,17,5.5,not_published
1540 +Chertsey,62047,Lanaudière,plex,1.0,17,5.8,not_published
1541 +Brébeuf,78075,Laurentides,plex,1.0,17,5.8,not_published
1542 +Shefford,47035,Estrie,plex,1.0,17,5.8,not_published
1543 +Saint-Médard,11025,Bas-Saint-Laurent,unifamilial,1.0,17,5.5,not_published
1544 +Saint-Pamphile,17010,Chaudière-Appalaches,plex,1.0,17,5.8,not_published
1545 +Les Escoumins,95025,Côte-Nord,plex,1.0,17,5.8,not_published
1546 +Saint-Pacôme,14070,Bas-Saint-Laurent,plex,1.0,17,4.5,not_published
1547 +Aguanish,98030,Côte-Nord,unifamilial,1.0,17,5.8,not_published
1548 +Sacré-Coeur,95010,Côte-Nord,plex,1.0,17,4.5,not_published
1549 +Saint-Philibert,29065,Chaudière-Appalaches,unifamilial,1.0,17,5.8,not_published
1550 +Paspébiac,05032,Gaspésie–Îles-de-la-Madeleine,plex,1.0,17,5.5,not_published
1551 +Saint-Hyacinthe,54048,Montérégie,condo,1.0,17,5.5,not_published
1552 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,condo,1.0,17,5.5,not_published
1553 +Farnham,46112,Estrie,condo,1.0,17,5.8,not_published
1554 +Saint-Alphonse-de-Granby,47010,Estrie,plex,1.0,17,5.5,not_published
1555 +Saint-Clet,71045,Montérégie,plex,1.0,16,5.5,not_published
1556 +Saint-Marc-sur-Richelieu,57050,Montérégie,plex,1.0,16,5.2,not_published
1557 +Saint-Gervais,19075,Chaudière-Appalaches,plex,1.0,16,5.2,not_published
1558 +Hudson,71100,Montérégie,plex,1.0,16,5.2,not_published
1559 +Saint-Gilles,33035,Chaudière-Appalaches,plex,1.0,16,5.5,not_published
1560 +Potton,45030,Estrie,plex,1.0,16,5.2,not_published
1561 +Stanbridge Station,46030,Estrie,unifamilial,1.0,16,4.5,not_published
1562 +Causapscal,07018,Bas-Saint-Laurent,plex,1.0,16,5.2,not_published
1563 +Nominingue,79030,Laurentides,plex,1.0,16,4.8,not_published
1564 +L'Île-d'Anticosti,98020,Côte-Nord,unifamilial,1.0,16,5.5,not_published
1565 +Saint-Cléophas,07090,Bas-Saint-Laurent,unifamilial,1.0,16,5.5,not_published
1566 +Brownsburg-Chatham,76043,Laurentides,condo,1.0,16,5.2,not_published
1567 +Saint-Germain-de-Kamouraska,14045,Bas-Saint-Laurent,unifamilial,1.0,16,4.8,not_published
1568 +Notre-Dame-du-Nord,85090,Abitibi-Témiscamingue,plex,1.0,16,5.2,not_published
1569 +Sainte-Émélie-de-l'Énergie,62070,Lanaudière,plex,1.0,16,5.5,not_published
1570 +Rivière-à-Claude,04020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,16,4.8,not_published
1571 +Saint-Liboire,54072,Montérégie,plex,1.0,16,5.2,not_published
1572 +Sainte-Hélène-de-Bagot,54095,Montérégie,plex,1.0,16,4.8,not_published
1573 +Natashquan,98025,Côte-Nord,unifamilial,1.0,16,5.5,not_published
1574 +Saint-Ulric,08073,Bas-Saint-Laurent,plex,1.0,16,5.5,not_published
1575 +Saint-Polycarpe,71020,Montérégie,condo,1.0,16,5.2,not_published
1576 +Belcourt,89050,Abitibi-Témiscamingue,unifamilial,1.0,16,5.2,not_published
1577 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,plex,1.0,15,4.8,not_published
1578 +Notre-Dame-des-Prairies,61030,Lanaudière,condo,1.0,15,4.8,not_published
1579 +Saint-Donat,62060,Lanaudière,condo,1.0,15,3.4,not_published
1580 +Léry,67055,Montérégie,plex,1.0,15,4.5,not_published
1581 +Franklin,69010,Montérégie,plex,1.0,15,5.2,not_published
1582 +Saint-Étienne-des-Grès,51090,Mauricie,plex,1.0,15,4.8,not_published
1583 +Port-Daniel–Gascons,02047,Gaspésie–Îles-de-la-Madeleine,plex,1.0,15,4.8,not_published
1584 +Lac-Bouchette,91005,Saguenay–Lac-Saint-Jean,plex,1.0,15,4.8,not_published
1585 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,plex,1.0,15,5.2,not_published
1586 +Authier,87050,Abitibi-Témiscamingue,unifamilial,1.0,15,5.2,not_published
1587 +Lemieux,38020,Centre-du-Québec,unifamilial,1.0,15,4.5,not_published
1588 +Coteau-du-Lac,71040,Montérégie,condo,1.0,15,5.2,not_published
1589 +Saint-Cuthbert,52062,Lanaudière,plex,1.0,15,5.2,not_published
1590 +Saint-Zacharie,28005,Chaudière-Appalaches,plex,1.0,15,5.2,not_published
1591 +Ripon,80078,Outaouais,plex,1.0,15,5.2,not_published
1592 +Saint-Denis-sur-Richelieu,57068,Montérégie,plex,1.0,15,4.8,not_published
1593 +Baie-D'Urfé,66112,Montréal,condo,1.0,15,5.2,not_published
1594 +Shigawake,05010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,14,4.8,not_published
1595 +Pont-Rouge,34017,Capitale-Nationale,condo,1.0,14,4.8,not_published
1596 +Lac-Poulin,29095,Chaudière-Appalaches,unifamilial,1.0,14,4.8,not_published
1597 +Compton,44071,Estrie,plex,1.0,14,4.8,not_published
1598 +Hampstead,66062,Montréal,condo,1.0,14,4.5,not_published
1599 +Maria,06005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,14,4.8,not_published
1600 +Saint-Gabriel-de-Brandon,52085,Lanaudière,plex,1.0,14,4.8,not_published
1601 +Saint-Anicet,69070,Montérégie,plex,1.0,14,4.8,not_published
1602 +Sayabec,07085,Bas-Saint-Laurent,plex,1.0,14,4.8,not_published
1603 +L'Ascension-de-Notre-Seigneur,93065,Saguenay–Lac-Saint-Jean,plex,1.0,14,4.8,not_published
1604 +Stanstead,45025,Estrie,plex,1.0,14,4.5,not_published
1605 +Napierville,68030,Montérégie,condo,1.0,14,4.1,not_published
1606 +Saint-Édouard-de-Lotbinière,33080,Chaudière-Appalaches,plex,1.0,14,4.8,not_published
1607 +Mont-Saint-Pierre,04015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,14,4.8,not_published
1608 +Durham-Sud,49015,Centre-du-Québec,plex,1.0,14,4.8,not_published
1609 +Saint-Pierre,61020,Lanaudière,unifamilial,1.0,14,4.8,not_published
1610 +Saint-Alexis,63023,Lanaudière,plex,1.0,14,4.5,not_published
1611 +Chute-aux-Outardes,96035,Côte-Nord,plex,1.0,14,4.5,not_published
1612 +Nouvelle,06020,Gaspésie–Îles-de-la-Madeleine,plex,1.0,14,4.8,not_published
1613 +Saint-Isidore,26063,Chaudière-Appalaches,plex,1.0,14,4.8,not_published
1614 +Entrelacs,62053,Lanaudière,plex,1.0,13,4.1,not_published
1615 +Saint-Urbain,16055,Capitale-Nationale,plex,1.0,13,4.5,not_published
1616 +Saint-Gilles,33035,Chaudière-Appalaches,condo,1.0,13,4.5,not_published
1617 +Armagh,19037,Chaudière-Appalaches,plex,1.0,13,4.5,not_published
1618 +Messines,83060,Outaouais,plex,1.0,13,4.5,not_published
1619 +Mille-Isles,76030,Laurentides,plex,1.0,13,3.8,not_published
1620 +Tingwick,39025,Centre-du-Québec,plex,1.0,13,4.5,not_published
1621 +Saint-Michel-de-Bellechasse,19110,Chaudière-Appalaches,plex,1.0,13,4.5,not_published
1622 +Launay,88080,Abitibi-Témiscamingue,unifamilial,1.0,13,4.5,not_published
1623 +Austin,45085,Estrie,plex,1.0,13,4.5,not_published
1624 +Albanel,92030,Saguenay–Lac-Saint-Jean,plex,1.0,13,4.1,not_published
1625 +Saint-Liguori,63065,Lanaudière,plex,1.0,13,4.1,not_published
1626 +Lac-Mégantic,30030,Estrie,condo,1.0,13,4.5,not_published
1627 +Lawrenceville,42045,Estrie,plex,1.0,13,4.1,not_published
1628 +Berthier-sur-Mer,18065,Chaudière-Appalaches,plex,1.0,13,4.5,not_published
1629 +Saint-Stanislas-de-Kostka,70040,Montérégie,plex,1.0,13,4.1,not_published
1630 +Sainte-Geneviève-de-Berthier,52040,Lanaudière,plex,1.0,13,4.5,not_published
1631 +Béarn,85020,Abitibi-Témiscamingue,plex,1.0,13,4.1,not_published
1632 +Sainte-Perpétue,17030,Chaudière-Appalaches,plex,1.0,13,4.1,not_published
1633 +Saint-Alexandre,56055,Montérégie,plex,1.0,13,4.5,not_published
1634 +Saint-Honoré-de-Témiscouata,13090,Bas-Saint-Laurent,plex,1.0,13,4.1,not_published
1635 +Saint-Roch-de-l'Achigan,63035,Lanaudière,condo,1.0,13,4.5,not_published
1636 +Sainte-Lucie-des-Laurentides,78020,Laurentides,plex,1.0,13,3.8,not_published
1637 +Saint-Godefroi,05015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,13,4.5,not_published
1638 +La Morandière-Rochebaucourt,88012,Abitibi-Témiscamingue,unifamilial,1.0,13,4.1,not_published
1639 +Belleterre,85065,Abitibi-Témiscamingue,unifamilial,1.0,13,4.5,not_published
1640 +Rosemère,73020,Laurentides,condo,1.0,13,4.1,not_published
1641 +Saint-Louis-du-Ha! Ha!,13080,Bas-Saint-Laurent,plex,1.0,13,3.8,not_published
1642 +Fort-Coulonge,84060,Outaouais,plex,1.0,13,4.5,not_published
1643 +Frelighsburg,46010,Estrie,plex,1.0,13,4.5,not_published
1644 +Sainte-Béatrix,62020,Lanaudière,plex,1.0,13,4.5,not_published
1645 +Sheenboro,84095,Outaouais,unifamilial,1.0,13,4.5,not_published
1646 +Vallée-Jonction,26015,Chaudière-Appalaches,plex,1.0,13,3.8,not_published
1647 +Venise-en-Québec,56005,Montérégie,condo,1.0,12,4.1,not_published
1648 +Saint-Victor,27008,Chaudière-Appalaches,plex,1.0,12,3.8,not_published
1649 +Saint-Bernard,26055,Chaudière-Appalaches,plex,1.0,12,4.1,not_published
1650 +Plaisance,80045,Outaouais,plex,1.0,12,4.1,not_published
1651 +Lac-Simon,80095,Outaouais,plex,1.0,12,4.1,not_published
1652 +Wotton,40017,Estrie,plex,1.0,12,4.1,not_published
1653 +Saint-Jean-de-Cherbourg,08010,Bas-Saint-Laurent,unifamilial,1.0,12,4.1,not_published
1654 +Lambton,30095,Estrie,plex,1.0,12,4.1,not_published
1655 +Saint-André-de-Restigouche,06040,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,12,3.8,not_published
1656 +Saint-Étienne-de-Beauharnois,70030,Montérégie,plex,1.0,12,4.1,not_published
1657 +Godmanchester,69060,Montérégie,plex,1.0,12,4.1,not_published
1658 +Nicolet,50072,Centre-du-Québec,condo,1.0,12,4.1,not_published
1659 +Saint-Damien-de-Buckland,19030,Chaudière-Appalaches,plex,1.0,12,4.1,not_published
1660 +Saint-Elphège,50095,Centre-du-Québec,unifamilial,1.0,12,4.1,not_published
1661 +Otter Lake,84055,Outaouais,plex,1.0,12,4.1,not_published
1662 +Côte-Nord-du-Golfe-du-Saint-Laurent,98015,Côte-Nord,unifamilial,1.0,12,4.1,not_published
1663 +Adstock,31056,Chaudière-Appalaches,plex,1.0,12,4.1,not_published
1664 +L'Isle-aux-Coudres,16023,Capitale-Nationale,plex,1.0,12,4.1,not_published
1665 +Saint-Damase,54017,Montérégie,plex,1.0,12,3.8,not_published
1666 +Sainte-Marguerite-Marie,07005,Bas-Saint-Laurent,unifamilial,1.0,12,3.8,not_published
1667 +Saint-Antoine-de-Tilly,33095,Chaudière-Appalaches,plex,1.0,12,4.1,not_published
1668 +Sainte-Anne-de-Bellevue,66117,Montréal,condo,1.0,11,3.8,not_published
1669 +Saint-Alexandre-des-Lacs,07065,Bas-Saint-Laurent,unifamilial,1.0,11,3.8,not_published
1670 +Saint-Honoré-de-Shenley,29038,Chaudière-Appalaches,plex,1.0,11,3.8,not_published
1671 +Chelsea,82025,Outaouais,condo,1.0,11,3.1,not_published
1672 +Saint-Joseph-de-Coleraine,31045,Chaudière-Appalaches,plex,1.0,11,3.8,not_published
1673 +Saint-Guillaume,49113,Centre-du-Québec,plex,1.0,11,3.8,not_published
1674 +Percé,02005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,11,3.8,not_published
1675 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,condo,1.0,11,3.1,not_published
1676 +Saint-Benoît-Labre,29100,Chaudière-Appalaches,plex,1.0,11,3.4,not_published
1677 +Saint-Ferdinand,32013,Centre-du-Québec,plex,1.0,11,3.8,not_published
1678 +Campbell's Bay,84030,Outaouais,plex,1.0,11,3.4,not_published
1679 +Saint-Alban,34097,Capitale-Nationale,plex,1.0,11,3.8,not_published
1680 +Notre-Dame-de-Lorette,92060,Saguenay–Lac-Saint-Jean,unifamilial,1.0,11,3.8,not_published
1681 +Saint-Mathieu-de-Beloeil,57045,Montérégie,condo,1.0,11,3.8,not_published
1682 +Murdochville,03025,Gaspésie–Îles-de-la-Madeleine,plex,1.0,11,3.8,not_published
1683 +Lac-Walker,97904,Côte-Nord,unifamilial,1.0,11,3.8,not_published
1684 +Déléage,83070,Outaouais,plex,1.0,11,3.8,not_published
1685 +Baie-du-Febvre,50100,Centre-du-Québec,plex,1.0,11,3.8,not_published
1686 +Saint-Mathieu,67005,Montérégie,plex,1.0,11,3.8,not_published
1687 +La Minerve,78130,Laurentides,plex,1.0,11,3.8,not_published
1688 +Saint-Mathieu-de-Beloeil,57045,Montérégie,plex,1.0,11,3.8,not_published
1689 +Saint-Louis-de-Gonzague,70035,Montérégie,plex,1.0,11,3.8,not_published
1690 +Saint-Stanislas,37245,Mauricie,plex,1.0,11,3.8,not_published
1691 +Saint-Ours,53032,Montérégie,plex,1.0,11,3.8,not_published
1692 +Saint-Chrysostome,69017,Montérégie,plex,1.0,10,3.1,not_published
1693 +Rivière-Bleue,13025,Bas-Saint-Laurent,plex,1.0,10,3.1,not_published
1694 +Saint-Damien,62075,Lanaudière,plex,1.0,10,3.4,not_published
1695 +Ange-Gardien,55008,Montérégie,condo,1.0,10,3.1,not_published
1696 +Saint-Jude,54110,Montérégie,plex,1.0,10,3.4,not_published
1697 +Rapides-des-Joachims,84100,Outaouais,unifamilial,1.0,10,3.1,not_published
1698 +Taschereau,87042,Abitibi-Témiscamingue,plex,1.0,10,3.4,not_published
1699 +Saint-Venant-de-Paquette,44005,Estrie,unifamilial,1.0,10,3.4,not_published
1700 +Massueville,53010,Montérégie,plex,1.0,10,3.4,not_published
1701 +Sainte-Cécile-de-Milton,47055,Estrie,plex,1.0,10,3.1,not_published
1702 +Deschaillons-sur-Saint-Laurent,38070,Centre-du-Québec,plex,1.0,10,3.4,not_published
1703 +Dudswell,41117,Estrie,plex,1.0,10,3.4,not_published
1704 +Palmarolle,87025,Abitibi-Témiscamingue,plex,1.0,10,3.4,not_published
1705 +Saint-Robert,53020,Montérégie,plex,1.0,10,3.4,not_published
1706 +Frelighsburg,46010,Estrie,condo,1.0,10,3.4,not_published
1707 +Batiscan,37210,Mauricie,plex,1.0,10,3.4,not_published
1708 +Saint-Roch-Ouest,63040,Lanaudière,unifamilial,1.0,10,3.1,not_published
1709 +Sainte-Brigide-d'Iberville,56105,Montérégie,plex,1.0,10,3.4,not_published
1710 +Montpellier,80090,Outaouais,plex,1.0,10,3.4,not_published
1711 +Chapais,99020,Nord-du-Québec,plex,1.0,10,3.4,not_published
1712 +Saint-Antoine-sur-Richelieu,57075,Montérégie,plex,1.0,10,3.4,not_published
1713 +Lachute,76020,Laurentides,condo,1.0,10,3.4,not_published
1714 +Matagami,99015,Nord-du-Québec,plex,1.0,10,3.4,not_published
1715 +Saint-Sylvestre,33007,Chaudière-Appalaches,plex,1.0,10,3.4,not_published
1716 +Val-Joli,42095,Estrie,plex,1.0,10,3.4,not_published
1717 +Bégin,94250,Saguenay–Lac-Saint-Jean,plex,1.0,10,3.4,not_published
1718 +Saint-Célestin,50030,Centre-du-Québec,plex,1.0,10,3.4,not_published
1719 +Notre-Dame-des-Sept-Douleurs,12045,Bas-Saint-Laurent,unifamilial,1.0,10,3.4,not_published
1720 +Laurierville,32072,Centre-du-Québec,plex,1.0,10,3.4,not_published
1721 +Bristol,84005,Outaouais,plex,1.0,10,2.7,not_published
1722 +Saint-Blaise-sur-Richelieu,56065,Montérégie,plex,1.0,10,3.1,not_published
1723 +Wentworth-Nord,77060,Laurentides,plex,1.0,10,3.1,not_published
1724 +Notre-Dame-du-Portage,12080,Bas-Saint-Laurent,plex,1.0,10,3.4,not_published
1725 +Saint-Charles-Garnier,09010,Bas-Saint-Laurent,unifamilial,1.0,10,3.4,not_published
1726 +Rivière-Saint-Jean,98050,Côte-Nord,unifamilial,1.0,10,3.4,not_published
1727 +Saint-Adelphe,35015,Mauricie,plex,1.0,10,3.4,not_published
1728 +Saint-Juste-du-Lac,13040,Bas-Saint-Laurent,unifamilial,1.0,10,3.4,not_published
1729 +Grand-Remous,83095,Outaouais,plex,1.0,10,3.4,not_published
1730 +Price,09065,Bas-Saint-Laurent,plex,1.0,10,3.1,not_published
1731 +Saint-Michel,68050,Montérégie,plex,1.0,10,3.4,not_published
1732 +Beaumont,19105,Chaudière-Appalaches,plex,1.0,10,3.4,not_published
1733 +Saint-Henri-de-Taillon,93070,Saguenay–Lac-Saint-Jean,plex,1.0,9,3.1,not_published
1734 +Saint-Magloire,28075,Chaudière-Appalaches,plex,1.0,9,3.1,not_published
1735 +Sainte-Monique,93075,Saguenay–Lac-Saint-Jean,plex,1.0,9,3.1,not_published
1736 +Lochaber-Partie-Ouest,80060,Outaouais,plex,1.0,9,3.1,not_published
1737 +Saint-Boniface,51085,Mauricie,condo,1.0,9,3.1,not_published
1738 +Kingsey Falls,39097,Centre-du-Québec,plex,1.0,9,3.1,not_published
1739 +Auclair,13045,Bas-Saint-Laurent,unifamilial,1.0,9,3.1,not_published
1740 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,plex,1.0,9,2.7,not_published
1741 +Montréal-Ouest,66047,Montréal,condo,1.0,9,3.1,not_published
1742 +Saint-Théodore-d'Acton,48045,Montérégie,plex,1.0,9,3.1,not_published
1743 +Girardville,92055,Saguenay–Lac-Saint-Jean,plex,1.0,9,3.1,not_published
1744 +Sainte-Anne-de-Sabrevois,56060,Montérégie,plex,1.0,9,3.1,not_published
1745 +Saint-Cyrille-de-Lessard,17045,Chaudière-Appalaches,plex,1.0,9,3.1,not_published
1746 +Thetford Mines,31084,Chaudière-Appalaches,condo,1.0,9,3.1,not_published
1747 +Saint-Aimé-du-Lac-des-Îles,79022,Laurentides,plex,1.0,9,3.1,not_published
1748 +L'Isle-aux-Allumettes,84082,Outaouais,plex,1.0,9,3.1,not_published
1749 +Chesterville,39030,Centre-du-Québec,plex,1.0,9,3.1,not_published
1750 +L'Île-Dorval,66092,Montréal,unifamilial,1.0,9,3.1,not_published
1751 +L'Isle-Verte,12043,Bas-Saint-Laurent,plex,1.0,9,3.1,not_published
1752 +Saint-Hilarion,16050,Capitale-Nationale,plex,1.0,9,3.1,not_published
1753 +Petite-Vallée,03015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,9,3.1,not_published
1754 +Saint-Philémon,19005,Chaudière-Appalaches,plex,1.0,9,3.1,not_published
1755 +Val-Alain,33070,Chaudière-Appalaches,plex,1.0,9,3.1,not_published
1756 +Sainte-Clotilde,68020,Montérégie,plex,1.0,9,3.1,not_published
1757 +Mont-Blanc,78047,Laurentides,condo,1.0,9,3.1,not_published
1758 +Lac-Sainte-Marie,83020,Outaouais,plex,1.0,9,2.7,not_published
1759 +Kingsbury,42070,Estrie,unifamilial,1.0,9,3.1,not_published
1760 +Saint-Patrice-de-Sherrington,68025,Montérégie,plex,1.0,9,2.7,not_published
1761 +Hemmingford,68010,Montérégie,plex,1.0,9,2.7,not_published
1762 +Stanbridge East,46045,Estrie,plex,1.0,9,3.1,not_published
1763 +Saint-Charles-sur-Richelieu,57057,Montérégie,plex,1.0,9,2.7,not_published
1764 +Saint-Léonard-de-Portneuf,34115,Capitale-Nationale,plex,1.0,9,3.1,not_published
1765 +Frontenac,30025,Estrie,plex,1.0,9,3.1,not_published
1766 +Bouchette,83050,Outaouais,plex,1.0,9,3.1,not_published
1767 +Tring-Jonction,27060,Chaudière-Appalaches,plex,1.0,9,3.1,not_published
1768 +Les Éboulements,16048,Capitale-Nationale,plex,1.0,9,2.7,not_published
1769 +Lotbinière,33115,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1770 +Mont-Carmel,14005,Bas-Saint-Laurent,plex,1.0,8,2.4,not_published
1771 +Sainte-Hélène-de-Kamouraska,14025,Bas-Saint-Laurent,plex,1.0,8,2.7,not_published
1772 +Bury,41070,Estrie,plex,1.0,8,2.4,not_published
1773 +Saint-Lucien,49030,Centre-du-Québec,plex,1.0,8,2.7,not_published
1774 +Saint-François-de-la-Rivière-du-Sud,18060,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1775 +Saint-Flavien,33052,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1776 +Gore,76025,Laurentides,plex,1.0,8,2.7,not_published
1777 +Saint-Lazare-de-Bellechasse,19050,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1778 +Lac-Drolet,30080,Estrie,plex,1.0,8,2.7,not_published
1779 +Stratford,30110,Estrie,plex,1.0,8,2.7,not_published
1780 +Sainte-Angèle-de-Monnoir,55030,Montérégie,plex,1.0,8,2.7,not_published
1781 +Notre-Dame-de-Pontmain,79010,Laurentides,plex,1.0,8,2.4,not_published
1782 +Saint-Séverin,35020,Mauricie,plex,1.0,8,2.7,not_published
1783 +Howick,69025,Montérégie,plex,1.0,8,2.7,not_published
1784 +La Patrie,41027,Estrie,plex,1.0,8,2.7,not_published
1785 +Saint-François-de-Sales,91015,Saguenay–Lac-Saint-Jean,plex,1.0,8,2.7,not_published
1786 +Sainte-Anne-de-la-Rochelle,42050,Estrie,plex,1.0,8,2.7,not_published
1787 +Saint-Simon,54090,Montérégie,plex,1.0,8,2.7,not_published
1788 +Kazabazua,83015,Outaouais,plex,1.0,8,2.7,not_published
1789 +Saint-Michel,68050,Montérégie,condo,1.0,8,2.7,not_published
1790 +Sainte-Flavie,09085,Bas-Saint-Laurent,plex,1.0,8,2.4,not_published
1791 +Disraeli,31020,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1792 +Cap-Chat,04047,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,2.7,not_published
1793 +Sainte-Hénédine,26040,Chaudière-Appalaches,plex,1.0,8,2.7,not_published
1794 +Saint-Aimé-des-Lacs,15030,Capitale-Nationale,plex,1.0,8,2.7,not_published
1795 +Notre-Dame-du-Bon-Conseil,49075,Centre-du-Québec,plex,1.0,8,2.4,not_published
1796 +Sainte-Marie-Salomé,63005,Lanaudière,plex,1.0,8,2.7,not_published
1797 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,condo,1.0,8,2.1,not_published
1798 +Lefebvre,49020,Centre-du-Québec,plex,1.0,8,2.7,not_published
1799 +Saint-Paul-de-l'Île-aux-Noix,56035,Montérégie,plex,1.0,8,2.7,not_published
1800 +Huberdeau,78065,Laurentides,plex,1.0,8,2.7,not_published
1801 +Pointe-à-la-Croix,06030,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,2.7,not_published
1802 +Saint-Valérien-de-Milton,54065,Montérégie,plex,1.0,8,2.7,not_published
1803 +Saint-Maxime-du-Mont-Louis,04010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,2.7,not_published
1804 +Saint-Ludger-de-Milot,93080,Saguenay–Lac-Saint-Jean,plex,1.0,8,2.7,not_published
1805 +Saint-Michel-des-Saints,62085,Lanaudière,plex,1.0,8,2.7,not_published
1806 +Henryville,56042,Montérégie,plex,1.0,8,2.7,not_published
1807 +Saint-Christophe-d'Arthabaska,39060,Centre-du-Québec,plex,1.0,8,2.7,not_published
1808 +Saint-Joseph-des-Érables,27050,Chaudière-Appalaches,unifamilial,1.0,7,2.1,not_published
1809 +Leclercville,33123,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1810 +Piopolis,30020,Estrie,plex,1.0,7,2.4,not_published
1811 +Saint-Ubalde,34090,Capitale-Nationale,plex,1.0,7,2.4,not_published
1812 +Saint-Édouard-de-Fabre,85015,Abitibi-Témiscamingue,plex,1.0,7,2.4,not_published
1813 +Bowman,80145,Outaouais,plex,1.0,7,2.4,not_published
1814 +Chute-Saint-Philippe,79065,Laurentides,plex,1.0,7,2.4,not_published
1815 +Sainte-Ursule,51040,Mauricie,plex,1.0,7,2.4,not_published
1816 +Nantes,30045,Estrie,plex,1.0,7,2.4,not_published
1817 +Saint-Côme,62065,Lanaudière,condo,1.0,7,2.4,not_published
1818 +Saint-Fabien-de-Panet,18015,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1819 +East Farnham,46085,Estrie,plex,1.0,7,2.1,not_published
1820 +Cap-Santé,34030,Capitale-Nationale,condo,1.0,7,2.4,not_published
1821 +Lac-des-Plages,80130,Outaouais,plex,1.0,7,2.4,not_published
1822 +La Durantaye,19090,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1823 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,condo,1.0,7,2.1,not_published
1824 +Ham-Nord,39010,Centre-du-Québec,plex,1.0,7,2.4,not_published
1825 +Valcourt,42060,Estrie,plex,1.0,7,2.4,not_published
1826 +Saint-Mathieu-du-Parc,51070,Mauricie,plex,1.0,7,2.4,not_published
1827 +Val-des-Bois,80140,Outaouais,plex,1.0,7,2.4,not_published
1828 +Baie-Comeau,96020,Côte-Nord,condo,1.0,7,2.4,not_published
1829 +Notre-Dame-de-la-Paix,80020,Outaouais,plex,1.0,7,2.4,not_published
1830 +Lac-au-Saumon,07057,Bas-Saint-Laurent,plex,1.0,7,2.4,not_published
1831 +Saint-Félix-de-Kingsey,49005,Centre-du-Québec,plex,1.0,7,2.4,not_published
1832 +Notre-Dame-de-Montauban,35005,Mauricie,plex,1.0,7,2.4,not_published
1833 +Val-Morin,78005,Laurentides,condo,1.0,7,2.4,not_published
1834 +Laverlochère-Angliers,85052,Abitibi-Témiscamingue,plex,1.0,7,1.7,not_published
1835 +Fossambault-sur-le-Lac,22010,Capitale-Nationale,plex,1.0,7,2.4,not_published
1836 +Lorraine,73025,Laurentides,plex,1.0,7,2.4,not_published
1837 +New Carlisle,05040,Gaspésie–Îles-de-la-Madeleine,plex,1.0,7,2.1,not_published
1838 +Labrecque,93055,Saguenay–Lac-Saint-Jean,plex,1.0,7,2.4,not_published
1839 +Sainte-Jeanne-d'Arc,92015,Saguenay–Lac-Saint-Jean,plex,1.0,7,2.4,not_published
1840 +Saint-Malachie,19025,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1841 +Saint-Modeste,12020,Bas-Saint-Laurent,plex,1.0,7,2.4,not_published
1842 +Sainte-Germaine-Boulé,87030,Abitibi-Témiscamingue,plex,1.0,7,2.4,not_published
1843 +Cayamant,83040,Outaouais,plex,1.0,7,2.4,not_published
1844 +Saint-Agapit,33045,Chaudière-Appalaches,condo,1.0,7,2.4,not_published
1845 +Champneuf,88005,Abitibi-Témiscamingue,unifamilial,1.0,7,2.4,not_published
1846 +Thurso,80050,Outaouais,condo,1.0,7,2.4,not_published
1847 +Baie-Johan-Beetz,98035,Côte-Nord,unifamilial,1.0,7,2.4,not_published
1848 +North Hatley,45050,Estrie,plex,1.0,7,2.4,not_published
1849 +Sainte-Félicité,08023,Bas-Saint-Laurent,plex,1.0,7,2.4,not_published
1850 +Saint-Paul-de-Montminy,18030,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1851 +Sainte-Angèle-de-Mérici,09035,Bas-Saint-Laurent,plex,1.0,7,2.4,not_published
1852 +Grande-Vallée,03020,Gaspésie–Îles-de-la-Madeleine,plex,1.0,7,2.4,not_published
1853 +Saint-Elzéar,26022,Chaudière-Appalaches,plex,1.0,7,2.4,not_published
1854 +Saint-Irénée,15005,Capitale-Nationale,plex,1.0,7,2.4,not_published
1855 +Saint-Bernard-de-Lacolle,68005,Montérégie,plex,1.0,6,2.1,not_published
1856 +Pointe-des-Cascades,71055,Montérégie,condo,1.0,6,2.1,not_published
1857 +Cleveland,42110,Estrie,plex,1.0,6,2.1,not_published
1858 +Ayer's Cliff,45035,Estrie,condo,1.0,6,2.1,not_published
1859 +Saint-Clet,71045,Montérégie,condo,1.0,6,2.1,not_published
1860 +Saint-Hilaire-de-Dorset,29020,Chaudière-Appalaches,unifamilial,1.0,6,2.1,not_published
1861 +Sainte-Marguerite,26035,Chaudière-Appalaches,plex,1.0,6,2.1,not_published
1862 +Namur,80110,Outaouais,plex,1.0,6,1.7,not_published
1863 +Saint-Roch-des-Aulnaies,17065,Chaudière-Appalaches,plex,1.0,6,2.1,not_published
1864 +Lantier,78015,Laurentides,plex,1.0,6,2.1,not_published
1865 +Petit-Saguenay,94205,Saguenay–Lac-Saint-Jean,plex,1.0,6,2.1,not_published
1866 +Landrienne,88035,Abitibi-Témiscamingue,plex,1.0,6,2.1,not_published
1867 +L'Île-Cadieux,71095,Montérégie,unifamilial,1.0,6,2.1,not_published
1868 +Natashquan,98025,Côte-Nord,plex,1.0,6,2.1,not_published
1869 +Hudson,71100,Montérégie,condo,1.0,6,1.7,not_published
1870 +Saint-Gabriel-de-Rimouski,09025,Bas-Saint-Laurent,plex,1.0,6,2.1,not_published
1871 +Val-Saint-Gilles,87105,Abitibi-Témiscamingue,unifamilial,1.0,6,2.1,not_published
1872 +Hatley,45055,Estrie,plex,1.0,6,2.1,not_published
1873 +Saint-Urbain-Premier,70005,Montérégie,plex,1.0,6,1.7,not_published
1874 +Vaudreuil-sur-le-Lac,71090,Montérégie,plex,1.0,6,2.1,not_published
1875 +Saint-Félix-d'Otis,94225,Saguenay–Lac-Saint-Jean,plex,1.0,6,2.1,not_published
1876 +Saint-Majorique-de-Grantham,49095,Centre-du-Québec,plex,1.0,6,2.1,not_published
1877 +Lac-du-Cerf,79015,Laurentides,plex,1.0,6,2.1,not_published
1878 +Manseau,38028,Centre-du-Québec,plex,1.0,6,2.1,not_published
1879 +Warden,47030,Estrie,plex,1.0,6,2.1,not_published
1880 +Saint-Charles-Borromée,61035,Lanaudière,condo,1.0,6,2.1,not_published
1881 +Beaumont,19105,Chaudière-Appalaches,condo,1.0,6,2.1,not_published
1882 +Saint-Cyprien,28040,Chaudière-Appalaches,plex,1.0,6,2.1,not_published
1883 +Saint-Valérien,10060,Bas-Saint-Laurent,plex,1.0,6,2.1,not_published
1884 +Blanc-Sablon,98005,Côte-Nord,plex,1.0,6,2.1,not_published
1885 +Parisville,38055,Centre-du-Québec,plex,1.0,6,2.1,not_published
1886 +Donnacona,34025,Capitale-Nationale,condo,1.0,6,2.1,not_published
1887 +Bonsecours,42040,Estrie,plex,1.0,6,2.1,not_published
1888 +Lac-Supérieur,78095,Laurentides,condo,1.0,6,2.1,not_published
1889 +Laforce,85070,Abitibi-Témiscamingue,unifamilial,1.0,6,2.1,not_published
1890 +Normétal,87115,Abitibi-Témiscamingue,plex,1.0,6,2.1,not_published
1891 +Lac-Chicobi,88904,Abitibi-Témiscamingue,unifamilial,2.0,6,1.0,not_published
1892 +Sainte-Pétronille,20030,Capitale-Nationale,plex,1.0,6,2.1,not_published
1893 +Hinchinbrooke,69045,Montérégie,plex,1.0,6,2.1,not_published
1894 +Saint-René,29050,Chaudière-Appalaches,plex,1.0,6,2.1,not_published
1895 +Saint-Cyprien-de-Napierville,68035,Montérégie,plex,1.0,6,2.1,not_published
1896 +Hérouxville,35035,Mauricie,plex,1.0,6,2.1,not_published
1897 +Bonne-Espérance,98010,Côte-Nord,unifamilial,1.0,6,2.1,not_published
1898 +Arundel,78060,Laurentides,plex,1.0,6,2.1,not_published
1899 +Notre-Dame-de-la-Salette,80087,Outaouais,plex,1.0,6,2.1,not_published
1900 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,condo,1.0,6,2.1,not_published
1901 +Honfleur,19070,Chaudière-Appalaches,plex,1.0,6,2.1,not_published
1902 +Sainte-Julienne,63060,Lanaudière,condo,1.0,6,2.1,not_published
1903 +Maddington Falls,39165,Centre-du-Québec,plex,1.0,6,2.1,not_published
1904 +Saint-Élie-de-Caxton,51075,Mauricie,plex,1.0,6,1.7,not_published
1905 +Saint-Hugues,54100,Montérégie,plex,1.0,6,1.7,not_published
1906 +Clarenceville,56010,Montérégie,plex,1.0,6,2.1,not_published
1907 +Verchères,59025,Montérégie,condo,1.0,6,2.1,not_published
1908 +Delson,67025,Montérégie,condo,1.0,6,2.1,not_published
1909 +Rougemont,55037,Montérégie,condo,1.0,6,1.7,not_published
1910 +La Doré,91050,Saguenay–Lac-Saint-Jean,plex,1.0,6,2.1,not_published
1911 +Notre-Dame-de-la-Merci,62055,Lanaudière,plex,1.0,6,2.1,not_published
1912 +Charette,51080,Mauricie,plex,1.0,6,2.1,not_published
1913 +Ayer's Cliff,45035,Estrie,plex,1.0,6,2.1,not_published
1914 +Saint-Armand,46017,Estrie,plex,1.0,6,1.7,not_published
1915 +Sainte-Angèle-de-Prémont,51055,Mauricie,plex,1.0,5,1.7,not_published
1916 +Saint-Alexis-de-Matapédia,06050,Gaspésie–Îles-de-la-Madeleine,plex,1.0,5,1.7,not_published
1917 +Saint-Luc-de-Bellechasse,28060,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1918 +Val-Brillant,07080,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1919 +Sainte-Perpétue,50050,Centre-du-Québec,plex,1.0,5,1.7,not_published
1920 +L'Ascension-de-Patapédia,06060,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,5,1.7,not_published
1921 +Lac-Delage,22030,Capitale-Nationale,plex,1.0,5,1.7,not_published
1922 +Saint-Philippe-de-Néri,14060,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1923 +Estérel,77011,Laurentides,condo,1.0,5,1.7,not_published
1924 +Mont-Saint-Michel,79110,Laurentides,plex,1.0,5,1.7,not_published
1925 +Waltham,84070,Outaouais,plex,1.0,5,1.7,not_published
1926 +Saint-Camille-de-Lellis,28070,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1927 +Saint-Sixte,80070,Outaouais,plex,1.0,5,1.7,not_published
1928 +Notre-Dame-des-Monts,15025,Capitale-Nationale,plex,1.0,5,1.7,not_published
1929 +Ragueneau,96040,Côte-Nord,plex,1.0,5,1.7,not_published
1930 +Saint-Césaire,55023,Montérégie,condo,1.0,5,1.4,not_published
1931 +La Corne,88030,Abitibi-Témiscamingue,plex,1.0,5,1.7,not_published
1932 +Saint-Romain,30100,Estrie,plex,1.0,5,1.7,not_published
1933 +Sainte-Hedwidge,91030,Saguenay–Lac-Saint-Jean,plex,1.0,5,1.4,not_published
1934 +Pohénégamook,13095,Bas-Saint-Laurent,condo,1.0,5,1.7,not_published
1935 +La Pocatière,14082,Bas-Saint-Laurent,condo,1.0,5,1.7,not_published
1936 +Saint-Zénon,62080,Lanaudière,plex,1.0,5,1.7,not_published
1937 +Saint-Sébastien,30085,Estrie,plex,1.0,5,1.7,not_published
1938 +Saint-Benjamin,28025,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1939 +Les Lacs-du-Témiscamingue,85907,Abitibi-Témiscamingue,unifamilial,1.0,5,1.7,not_published
1940 +Hemmingford,68015,Montérégie,condo,1.0,5,1.7,not_published
1941 +Saint-Vallier,19117,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1942 +Saint-Aubert,17055,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1943 +Stornoway,30105,Estrie,plex,1.0,5,1.7,not_published
1944 +Roberval,91025,Saguenay–Lac-Saint-Jean,condo,1.0,5,1.4,not_published
1945 +Montcalm,78055,Laurentides,plex,1.0,5,1.7,not_published
1946 +Sainte-Clotilde-de-Beauce,31060,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1947 +Duhamel-Ouest,85030,Abitibi-Témiscamingue,plex,1.0,5,1.7,not_published
1948 +Sainte-Eulalie,50005,Centre-du-Québec,plex,1.0,5,1.7,not_published
1949 +Saint-Hubert-de-Rivière-du-Loup,12010,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1950 +Matapédia,06045,Gaspésie–Îles-de-la-Madeleine,plex,1.0,5,1.7,not_published
1951 +Sainte-Rose-de-Watford,28030,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1952 +Saint-Émile-de-Suffolk,80125,Outaouais,plex,1.0,5,1.7,not_published
1953 +Racine,42032,Estrie,plex,1.0,5,1.7,not_published
1954 +Inverness,32058,Centre-du-Québec,plex,1.0,5,1.7,not_published
1955 +Saint-Claude,42100,Estrie,plex,1.0,5,1.7,not_published
1956 +Sainte-Marguerite,26035,Chaudière-Appalaches,condo,1.0,5,1.7,not_published
1957 +Les Bergeronnes,95018,Côte-Nord,plex,1.0,5,1.7,not_published
1958 +Val-David,78010,Laurentides,condo,1.0,5,1.7,not_published
1959 +Saint-Simon-les-Mines,29125,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1960 +Saint-Janvier-de-Joly,33065,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1961 +Saint-André-de-Kamouraska,14040,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1962 +Métis-sur-Mer,09048,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1963 +Saint-Wenceslas,50023,Centre-du-Québec,plex,1.0,5,1.7,not_published
1964 +Sainte-Geneviève-de-Batiscan,37215,Mauricie,plex,1.0,5,1.7,not_published
1965 +Brigham,46090,Estrie,plex,1.0,5,1.7,not_published
1966 +Scott,26048,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1967 +Dosquet,33040,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1968 +Notre-Dame-du-Bon-Conseil,49080,Centre-du-Québec,plex,1.0,5,1.7,not_published
1969 +Fassett,80005,Outaouais,plex,1.0,5,1.7,not_published
1970 +Saint-Mathieu-de-Rioux,11050,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1971 +Sainte-Anne-du-Lac,79115,Laurentides,plex,1.0,5,1.7,not_published
1972 +Grandes-Piles,35040,Mauricie,plex,1.0,5,1.7,not_published
1973 +Saint-Cyprien,12005,Bas-Saint-Laurent,plex,1.0,5,1.4,not_published
1974 +Saint-Jacques-le-Mineur,68040,Montérégie,plex,1.0,5,1.7,not_published
1975 +Saint-Pierre-de-Broughton,31135,Chaudière-Appalaches,plex,1.0,5,1.7,not_published
1976 +Belleterre,85065,Abitibi-Témiscamingue,plex,1.0,5,1.7,not_published
1977 +Sainte-Irène,07040,Bas-Saint-Laurent,plex,1.0,5,1.7,not_published
1978 +Saint-Augustin,98012,Côte-Nord,unifamilial,1.0,4,1.4,not_published
1979 +Saint-Isidore-de-Clifton,41012,Estrie,plex,1.0,4,1.4,not_published
1980 +Hemmingford,68015,Montérégie,plex,1.0,4,1.4,not_published
1981 +La Macaza,79047,Laurentides,plex,1.0,4,1.4,not_published
1982 +Richelieu,55057,Montérégie,condo,1.0,4,1.4,not_published
1983 +Trécesson,88075,Abitibi-Témiscamingue,plex,1.0,4,1.4,not_published
1984 +Shannon,22020,Capitale-Nationale,condo,1.0,4,1.4,not_published
1985 +Escuminac,06025,Gaspésie–Îles-de-la-Madeleine,plex,1.0,4,1.4,not_published
1986 +Saint-Narcisse-de-Rimouski,10015,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
1987 +Saint-Patrice-de-Beaurivage,33025,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
1988 +Notre-Dame-de-Stanbridge,46100,Estrie,plex,1.0,4,1.4,not_published
1989 +Mont-Albert,04902,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,4,1.4,not_published
1990 +Trois-Rives,35055,Mauricie,plex,1.0,4,1.4,not_published
1991 +Saint-Louis-de-Gonzague,70035,Montérégie,condo,1.0,4,1.4,not_published
1992 +Saint-Mathieu-du-Parc,51070,Mauricie,condo,1.0,4,1.4,not_published
1993 +Péribonka,92010,Saguenay–Lac-Saint-Jean,plex,1.0,4,1.4,not_published
1994 +Notre-Dame-de-Bonsecours,80015,Outaouais,plex,1.0,4,1.4,not_published
1995 +L'Épiphanie,60037,Lanaudière,condo,1.0,4,1.4,not_published
1996 +Saint-Michel-du-Squatec,13065,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
1997 +Roxton,48015,Montérégie,plex,1.0,4,1.4,not_published
1998 +Saint-Valère,39135,Centre-du-Québec,plex,1.0,4,1.4,not_published
1999 +Saint-Adrien,40010,Estrie,plex,1.0,4,1.4,not_published
2000 +Saint-Sébastien,56050,Montérégie,plex,1.0,4,1.4,not_published
2001 +Saint-Odilon-de-Cranbourne,27035,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2002 +Lac-Saint-Joseph,22015,Capitale-Nationale,plex,1.0,4,1.4,not_published
2003 +Sainte-Agathe-de-Lotbinière,33017,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2004 +Noyan,56015,Montérégie,plex,1.0,4,1.4,not_published
2005 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,condo,1.0,4,1.4,not_published
2006 +Lac-Casault,07908,Bas-Saint-Laurent,unifamilial,1.0,4,1.0,not_published
2007 +Sainte-Cécile-de-Whitton,30050,Estrie,plex,1.0,4,1.4,not_published
2008 +Frampton,26005,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2009 +Rivière-Bonaventure,05902,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,4,1.4,not_published
2010 +L'Avenir,49025,Centre-du-Québec,plex,1.0,4,1.4,not_published
2011 +Barnston-Ouest,44045,Estrie,plex,1.0,4,1.4,not_published
2012 +Sainte-Marthe,71110,Montérégie,plex,1.0,4,1.4,not_published
2013 +Saint-Vianney,07075,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
2014 +Baie-des-Sables,08080,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
2015 +Saint-François-d'Assise,06055,Gaspésie–Îles-de-la-Madeleine,plex,1.0,4,1.4,not_published
2016 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,condo,1.0,4,1.4,not_published
2017 +Bolton-Ouest,46065,Estrie,plex,1.0,4,1.4,not_published
2018 +Notre-Dame-des-Neiges,11045,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
2019 +Scotstown,41080,Estrie,plex,1.0,4,1.4,not_published
2020 +Saint-Léon-de-Standon,19020,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2021 +Packington,13015,Bas-Saint-Laurent,plex,1.0,4,1.4,not_published
2022 +Tadoussac,95005,Côte-Nord,plex,1.0,4,1.4,not_published
2023 +Coaticook,44037,Estrie,condo,1.0,4,1.4,not_published
2024 +Nédélec,85100,Abitibi-Témiscamingue,plex,1.0,4,1.0,not_published
2025 +Blue Sea,83045,Outaouais,plex,1.0,4,1.4,not_published
2026 +La Visitation-de-l'Île-Dupas,52050,Lanaudière,plex,1.0,4,1.4,not_published
2027 +Newport,41037,Estrie,plex,1.0,4,1.4,not_published
2028 +Saint-Ferdinand,32013,Centre-du-Québec,condo,1.0,4,1.4,not_published
2029 +Courcelles–Saint-Évariste,29027,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2030 +Bolton-Est,45095,Estrie,plex,1.0,4,1.4,not_published
2031 +Saint-Étienne-de-Bolton,45100,Estrie,plex,1.0,4,1.4,not_published
2032 +Neuville,34007,Capitale-Nationale,condo,1.0,4,1.4,not_published
2033 +Saint-Sylvère,38005,Centre-du-Québec,plex,1.0,4,1.4,not_published
2034 +Sainte-Apolline-de-Patton,18025,Chaudière-Appalaches,plex,1.0,4,1.0,not_published
2035 +Kirkland,66102,Montréal,plex,1.0,4,1.4,not_published
2036 +Saint-Albert,39085,Centre-du-Québec,plex,1.0,4,1.4,not_published
2037 +Saint-Théophile,29005,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2038 +Sainte-Aurélie,28015,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2039 +Saint-Georges-de-Windsor,40032,Estrie,plex,1.0,4,1.4,not_published
2040 +Alleyn-et-Cawood,84050,Outaouais,plex,1.0,4,1.4,not_published
2041 +Sainte-Louise,17060,Chaudière-Appalaches,plex,1.0,4,1.4,not_published
2042 +Sainte-Claire,19055,Chaudière-Appalaches,condo,1.0,4,1.4,not_published
2043 +Duparquet,87005,Abitibi-Témiscamingue,plex,1.0,4,1.4,not_published
2044 +Saint-Nazaire-d'Acton,48050,Montérégie,plex,1.0,4,1.4,not_published
2045 +Waterloo,47025,Estrie,condo,1.0,4,1.4,not_published
2046 +Egan-Sud,83075,Outaouais,plex,1.0,4,1.4,not_published
2047 +Dixville,44023,Estrie,plex,1.0,4,1.4,not_published
2048 +Saint-Ludger,30072,Estrie,plex,1.0,4,1.4,not_published
2049 +Hatley,45043,Estrie,plex,1.0,4,1.4,not_published
2050 +Saint-Elzéar,26022,Chaudière-Appalaches,condo,1.0,3,1.0,not_published
2051 +Westbury,41065,Estrie,plex,1.0,3,1.0,not_published
2052 +Saint-Clément,11005,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2053 +L'Île-du-Grand-Calumet,84035,Outaouais,plex,1.0,3,1.0,not_published
2054 +Sainte-Hélène-de-Bagot,54095,Montérégie,condo,1.0,3,1.0,not_published
2055 +Saint-Jacques-de-Leeds,31140,Chaudière-Appalaches,plex,1.0,3,1.0,not_published
2056 +Sainte-Sophie-de-Lévrard,38040,Centre-du-Québec,plex,1.0,3,1.0,not_published
2057 +Thorne,84045,Outaouais,plex,1.0,3,1.0,not_published
2058 +Saint-Nérée-de-Bellechasse,19045,Chaudière-Appalaches,plex,1.0,3,1.0,not_published
2059 +Melbourne,42075,Estrie,plex,1.0,3,1.0,not_published
2060 +Boileau,80115,Outaouais,plex,1.0,3,1.0,not_published
2061 +Saint-Donat,09030,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2062 +Notre-Dame-de-Lourdes,32080,Centre-du-Québec,plex,1.0,3,1.0,not_published
2063 +Sainte-Rita,11015,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2064 +Saint-Marc-du-Lac-Long,13020,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2065 +Matane,08053,Bas-Saint-Laurent,condo,1.0,3,1.0,not_published
2066 +Brome,46070,Estrie,plex,1.0,3,1.0,not_published
2067 +Saint-Bonaventure,49125,Centre-du-Québec,plex,1.0,3,1.0,not_published
2068 +Lac-Saguay,79060,Laurentides,plex,1.0,3,1.0,not_published
2069 +Sagard,15904,Capitale-Nationale,unifamilial,1.5,3,0.7,not_published
2070 +Saint-Marc-de-Figuery,88040,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2071 +L'Islet,17078,Chaudière-Appalaches,condo,1.0,3,1.0,not_published
2072 +Lac-Sergent,34120,Capitale-Nationale,plex,1.0,3,1.0,not_published
2073 +Lejeune,13050,Bas-Saint-Laurent,unifamilial,1.0,3,1.0,not_published
2074 +Saint-Gabriel,52080,Lanaudière,condo,1.0,3,1.0,not_published
2075 +Pointe-Lebel,96025,Côte-Nord,plex,1.0,3,1.0,not_published
2076 +Saint-Pierre,61020,Lanaudière,plex,1.0,3,1.0,not_published
2077 +Saint-François-Xavier-de-Viger,12025,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2078 +Longue-Rive,95032,Côte-Nord,plex,1.0,3,1.0,not_published
2079 +Saint-Just-de-Bretenières,18005,Chaudière-Appalaches,plex,1.0,3,1.0,not_published
2080 +Gros-Mécatina,98014,Côte-Nord,unifamilial,1.0,3,1.0,not_published
2081 +Château-Richer,21035,Capitale-Nationale,condo,1.0,3,1.0,not_published
2082 +Sainte-Clotilde,68020,Montérégie,condo,1.0,3,1.0,not_published
2083 +Sainte-Sabine,46105,Estrie,plex,1.0,3,1.0,not_published
2084 +Saint-Augustin,92005,Saguenay–Lac-Saint-Jean,plex,1.0,3,1.0,not_published
2085 +Montmagny,18050,Chaudière-Appalaches,condo,1.0,3,1.0,not_published
2086 +Eeyou Istchee Baie-James,99060,Nord-du-Québec,plex,1.0,3,1.0,not_published
2087 +Saint-Barnabé-Sud,54105,Montérégie,plex,1.0,3,1.0,not_published
2088 +Windsor,42088,Estrie,condo,1.0,3,1.0,not_published
2089 +Sainte-Florence,07010,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2090 +Bois-Franc,83085,Outaouais,plex,1.5,3,0.7,not_published
2091 +Sainte-Rose-du-Nord,94230,Saguenay–Lac-Saint-Jean,plex,1.0,3,1.0,not_published
2092 +Saint-Eugène-de-Ladrière,10075,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2093 +Grand-Saint-Esprit,50065,Centre-du-Québec,plex,1.0,3,1.0,not_published
2094 +Stanstead-Est,44050,Estrie,plex,1.0,3,1.0,not_published
2095 +Saint-Philémon,19005,Chaudière-Appalaches,condo,1.0,3,1.0,not_published
2096 +Moffet,85075,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2097 +Saint-Justin,51045,Mauricie,plex,1.0,3,1.0,not_published
2098 +Saint-Pierre-les-Becquets,38065,Centre-du-Québec,plex,1.0,3,1.0,not_published
2099 +Saint-Samuel,39130,Centre-du-Québec,plex,1.0,3,1.0,not_published
2100 +Saint-Frédéric,27065,Chaudière-Appalaches,plex,1.0,3,1.0,not_published
2101 +Schefferville,97040,Côte-Nord,unifamilial,1.0,3,1.0,not_published
2102 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,condo,1.0,3,1.0,not_published
2103 +Saint-Édouard,68045,Montérégie,plex,1.0,3,1.0,not_published
2104 +Très-Saint-Sacrement,69030,Montérégie,plex,1.0,3,1.0,not_published
2105 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,condo,1.0,3,1.0,not_published
2106 +Montcerf-Lytton,83088,Outaouais,plex,1.0,3,1.0,not_published
2107 +Saint-Jean-de-l'Île-d'Orléans,20015,Capitale-Nationale,plex,1.0,3,1.0,not_published
2108 +Saint-Luc-de-Vincennes,37225,Mauricie,plex,1.0,3,1.0,not_published
2109 +Saint-Léon-le-Grand,07030,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2110 +Saint-Prosper-de-Champlain,37250,Mauricie,plex,1.0,3,1.0,not_published
2111 +Saint-Robert-Bellarmin,30070,Estrie,plex,1.0,3,1.0,not_published
2112 +Rivière-Ouelle,14065,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2113 +Godbout,96010,Côte-Nord,plex,1.0,3,1.0,not_published
2114 +Fortierville,38047,Centre-du-Québec,plex,1.0,3,1.0,not_published
2115 +Dupuy,87085,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2116 +Sainte-Christine-d'Auvergne,34105,Capitale-Nationale,plex,1.0,3,1.0,not_published
2117 +Saint-Moïse,07095,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2118 +East Hereford,44010,Estrie,plex,1.0,3,1.0,not_published
2119 +Plessisville,32043,Centre-du-Québec,condo,1.0,3,1.0,not_published
2120 +Saint-Augustin-de-Woburn,30005,Estrie,plex,1.0,3,1.0,not_published
2121 +Dundee,69075,Montérégie,plex,1.0,3,1.0,not_published
2122 +Saint-Ignace-de-Stanbridge,46095,Estrie,plex,1.0,3,1.0,not_published
2123 +Rivière-Héva,89010,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2124 +Saint-Eugène,49105,Centre-du-Québec,plex,1.0,3,1.0,not_published
2125 +Saint-Norbert-d'Arthabaska,39043,Centre-du-Québec,plex,1.0,3,1.0,not_published
2126 +Senneterre,89045,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2127 +Saint-Eugène-de-Guigues,85085,Abitibi-Témiscamingue,plex,1.0,3,1.0,not_published
2128 +Lac-Simon,80095,Outaouais,condo,1.0,3,1.0,not_published
2129 +Ivry-sur-le-Lac,78042,Laurentides,plex,1.0,3,1.0,not_published
2130 +Litchfield,84040,Outaouais,plex,1.0,3,1.0,not_published
2131 +Sainte-Thérèse-de-Gaspé,02010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,3,1.0,not_published
2132 +Saint-Pierre-de-la-Rivière-du-Sud,18055,Chaudière-Appalaches,plex,1.0,3,1.0,not_published
2133 +Esprit-Saint,10005,Bas-Saint-Laurent,plex,1.0,3,1.0,not_published
2134 +Rivière-Ojima,87904,Abitibi-Témiscamingue,unifamilial,1.0,3,1.0,not_published
2135 +Bryson,84025,Outaouais,plex,1.0,3,1.0,not_published
2136 +Aumond,83090,Outaouais,plex,1.0,3,1.0,not_published
2137 +Saint-Épiphane,12030,Bas-Saint-Laurent,plex,1.5,3,0.7,not_published
2138 +Sainte-Lucie-de-Beauregard,18020,Chaudière-Appalaches,plex,1.0,2,0.7,not_published
2139 +Saint-Zéphirin-de-Courval,50090,Centre-du-Québec,plex,1.0,2,0.7,not_published
2140 +Poularies,87035,Abitibi-Témiscamingue,plex,1.0,2,0.7,not_published
2141 +Saint-André-du-Lac-Saint-Jean,91010,Saguenay–Lac-Saint-Jean,plex,1.0,2,0.7,not_published
2142 +Rapides-des-Joachims,84100,Outaouais,plex,1.0,2,0.7,not_published
2143 +Saint-Tharcisius,07070,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2144 +Larouche,94265,Saguenay–Lac-Saint-Jean,condo,1.0,2,0.7,not_published
2145 +Elgin,69050,Montérégie,plex,1.0,2,0.7,not_published
2146 +Saint-Didace,52090,Lanaudière,plex,1.0,2,0.7,not_published
2147 +Sainte-Paule,08040,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2148 +Low,83010,Outaouais,plex,1.0,2,0.7,not_published
2149 +Les Méchins,08005,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2150 +Sainte-Marie-de-Blandford,38015,Centre-du-Québec,plex,1.0,2,0.7,not_published
2151 +Longue-Pointe-de-Mingan,98045,Côte-Nord,plex,1.0,2,0.7,not_published
2152 +Saint-Michel-des-Saints,62085,Lanaudière,condo,1.0,2,0.7,not_published
2153 +Saint-Camille,40025,Estrie,plex,1.0,2,0.7,not_published
2154 +Saint-Antoine-de-l'Isle-aux-Grues,18070,Chaudière-Appalaches,plex,1.0,2,0.7,not_published
2155 +Saint-Roch-de-Mékinac,35045,Mauricie,plex,1.0,2,0.7,not_published
2156 +Notre-Dame-des-Bois,30010,Estrie,plex,1.0,2,0.7,not_published
2157 +Shigawake,05010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,0.7,not_published
2158 +Mulgrave-et-Derry,80085,Outaouais,plex,1.0,2,0.7,not_published
2159 +Saint-Mathias-sur-Richelieu,55065,Montérégie,condo,1.0,2,0.7,not_published
2160 +Sainte-Sophie-d'Halifax,32023,Centre-du-Québec,plex,1.0,2,0.7,not_published
2161 +Saint-Fortunat,31030,Chaudière-Appalaches,plex,1.0,2,0.7,not_published
2162 +Lochaber,80055,Outaouais,plex,1.0,2,0.7,not_published
2163 +Franquelin,96015,Côte-Nord,plex,1.0,2,0.7,not_published
2164 +Latulipe-et-Gaboury,85060,Abitibi-Témiscamingue,plex,1.0,2,0.7,not_published
2165 +Saint-Joachim-de-Shefford,47040,Estrie,plex,1.0,2,0.7,not_published
2166 +Baie-D'Urfé,66112,Montréal,plex,1.0,2,0.7,not_published
2167 +TNO aquatique de la MRC géographique de Montréal,66990,Montréal,unifamilial,1.0,2,0.7,not_published
2168 +Oka,72032,Laurentides,condo,1.0,2,0.7,not_published
2169 +Pointe-Fortune,71140,Montérégie,plex,1.0,2,0.7,not_published
2170 +Saint-Thomas,61027,Lanaudière,condo,1.0,2,0.7,not_published
2171 +Stanbridge Station,46030,Estrie,plex,1.0,2,0.7,not_published
2172 +Scott,26048,Chaudière-Appalaches,condo,1.0,2,0.7,not_published
2173 +Saint-Zénon-du-Lac-Humqui,07035,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2174 +Lac-Huron,10902,Bas-Saint-Laurent,unifamilial,1.0,2,0.7,not_published
2175 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,condo,1.0,2,0.7,not_published
2176 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,condo,1.0,2,0.7,not_published
2177 +Villeroy,32085,Centre-du-Québec,plex,1.0,2,0.7,not_published
2178 +L'Île-d'Anticosti,98020,Côte-Nord,plex,1.0,2,0.7,not_published
2179 +Crabtree,61013,Lanaudière,condo,1.0,2,0.7,not_published
2180 +Kiamika,79025,Laurentides,plex,1.0,2,0.7,not_published
2181 +Pike River,46025,Estrie,plex,1.0,2,0.7,not_published
2182 +Saint-François-de-l'Île-d'Orléans,20005,Capitale-Nationale,plex,1.0,2,0.7,not_published
2183 +Sainte-Victoire-de-Sorel,53025,Montérégie,condo,1.0,2,0.7,not_published
2184 +Colombier,95050,Côte-Nord,plex,1.0,2,0.7,not_published
2185 +Lamarche,93060,Saguenay–Lac-Saint-Jean,plex,1.0,2,0.7,not_published
2186 +Saint-Thomas-Didyme,92045,Saguenay–Lac-Saint-Jean,plex,1.0,2,0.7,not_published
2187 +Sainte-Madeleine-de-la-Rivière-Madeleine,04005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,0.7,not_published
2188 +Huntingdon,69055,Montérégie,condo,1.0,2,0.7,not_published
2189 +Notre-Dame-des-Sept-Douleurs,12045,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2190 +Chartierville,41020,Estrie,plex,1.0,2,0.7,not_published
2191 +Audet,30055,Estrie,plex,1.0,2,0.7,not_published
2192 +Biencourt,13055,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2193 +Harrington,76065,Laurentides,plex,1.0,2,0.7,not_published
2194 +Saint-Charles-de-Bourget,94260,Saguenay–Lac-Saint-Jean,plex,1.0,2,0.7,not_published
2195 +Notre-Dame-Auxiliatrice-de-Buckland,19010,Chaudière-Appalaches,plex,1.0,2,0.7,not_published
2196 +Wentworth,76035,Laurentides,plex,1.0,2,0.7,not_published
2197 +Rivière-à-Pierre,34135,Capitale-Nationale,plex,1.0,2,0.7,not_published
2198 +Saint-Barnabé,51025,Mauricie,plex,1.0,2,0.7,not_published
2199 +Saint-Edmond-de-Grantham,49100,Centre-du-Québec,plex,1.0,2,0.7,not_published
2200 +Saint-Joseph-de-Lepage,09070,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2201 +Sainte-Justine-de-Newton,71115,Montérégie,plex,1.0,2,0.7,not_published
2202 +Kingsbury,42070,Estrie,plex,1.0,2,0.7,not_published
2203 +Sainte-Brigitte-des-Saults,49085,Centre-du-Québec,plex,1.0,2,0.7,not_published
2204 +Saint-Stanislas-de-Kostka,70040,Montérégie,condo,1.0,2,0.7,not_published
2205 +Saint-Pierre-Baptiste,32050,Centre-du-Québec,plex,1.0,2,0.7,not_published
2206 +Chichester,84090,Outaouais,plex,1.0,2,0.7,not_published
2207 +Saint-Bernard,26055,Chaudière-Appalaches,condo,1.0,2,0.7,not_published
2208 +Senneville,66127,Montréal,plex,1.0,2,0.7,not_published
2209 +Fugèreville,85055,Abitibi-Témiscamingue,plex,1.0,2,0.7,not_published
2210 +Saint-Norbert,52070,Lanaudière,plex,1.0,2,0.7,not_published
2211 +Havelock,69005,Montérégie,plex,1.0,2,0.7,not_published
2212 +Saint-René-de-Matane,08035,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2213 +Saint-Éloi,11035,Bas-Saint-Laurent,plex,1.0,2,0.7,not_published
2214 +Saints-Anges,26010,Chaudière-Appalaches,plex,1.0,2,0.7,not_published
2215 +Saint-Elphège,50095,Centre-du-Québec,plex,1.0,2,0.7,not_published
2216 +Saint-Malachie,19025,Chaudière-Appalaches,condo,1.0,2,0.7,not_published
2217 +Lac-Masketsi,35902,Mauricie,unifamilial,1.0,2,0.7,not_published
2218 +Saint-Elzéar,05050,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,0.7,not_published
2219 +Sainte-Séraphine,39105,Centre-du-Québec,plex,1.0,2,0.7,not_published
2220 +Rémigny,85105,Abitibi-Témiscamingue,plex,1.0,2,0.7,not_published
2221 +Sainte-Gertrude-Manneville,88085,Abitibi-Témiscamingue,plex,1.0,2,0.7,not_published
2222 +Berry,88070,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2223 +Saint-Adolphe-d'Howard,77065,Laurentides,condo,1.0,1,0.3,not_published
2224 +Saint-Célestin,50035,Centre-du-Québec,plex,1.0,1,0.3,not_published
2225 +Sainte-Edwidge-de-Clifton,44055,Estrie,plex,1.0,1,0.3,not_published
2226 +Mayo,80065,Outaouais,plex,1.0,1,0.3,not_published
2227 +Saint-Venant-de-Paquette,44005,Estrie,plex,1.0,1,0.3,not_published
2228 +Saint-Guillaume-Nord,62912,Lanaudière,unifamilial,1.0,1,0.3,not_published
2229 +Saint-Octave-de-Métis,09055,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2230 +Routhierville,07902,Bas-Saint-Laurent,unifamilial,1.0,1,0.3,not_published
2231 +Sainte-Christine,48020,Montérégie,plex,1.0,1,0.3,not_published
2232 +Abercorn,46005,Estrie,plex,1.0,1,0.3,not_published
2233 +L'Ascension,79050,Laurentides,plex,1.0,1,0.3,not_published
2234 +Pointe-aux-Outardes,96030,Côte-Nord,plex,1.0,1,0.3,not_published
2235 +Saint-Herménégilde,44015,Estrie,plex,1.0,1,0.3,not_published
2236 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,condo,1.0,1,0.3,not_published
2237 +Saint-Dominique-du-Rosaire,88065,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2238 +Aston-Jonction,50013,Centre-du-Québec,plex,1.0,1,0.3,not_published
2239 +Berthierville,52035,Lanaudière,condo,1.0,1,0.3,not_published
2240 +Saint-Godefroi,05015,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2241 +Sainte-Élizabeth-de-Warwick,39090,Centre-du-Québec,plex,1.0,1,0.3,not_published
2242 +Portneuf-sur-Mer,95040,Côte-Nord,plex,1.0,1,0.3,not_published
2243 +Grosses-Roches,08015,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2244 +Sainte-Monique,50057,Centre-du-Québec,plex,1.0,1,0.3,not_published
2245 +Sainte-Élisabeth-de-Proulx,92903,Saguenay–Lac-Saint-Jean,unifamilial,1.0,1,0.3,not_published
2246 +Sainte-Félicité,17025,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2247 +Saint-Martin,29045,Chaudière-Appalaches,condo,1.0,1,0.3,not_published
2248 +Saint-Cléophas-de-Brandon,52075,Lanaudière,plex,1.0,1,0.3,not_published
2249 +Stukely-Sud,45105,Estrie,plex,1.0,1,0.3,not_published
2250 +Saint-Pie,54008,Montérégie,condo,1.0,1,0.3,not_published
2251 +Saint-Célestin,50030,Centre-du-Québec,condo,1.0,1,0.3,not_published
2252 +Clarendon,84015,Outaouais,plex,1.0,1,0.3,not_published
2253 +Linton,34904,Capitale-Nationale,unifamilial,1.0,1,0.3,not_published
2254 +Amos,88057,Abitibi-Témiscamingue,condo,1.0,1,0.3,not_published
2255 +Léry,67055,Montérégie,condo,1.0,1,0.3,not_published
2256 +Sainte-Thérèse-de-la-Gatineau,83055,Outaouais,plex,1.0,1,0.3,not_published
2257 +Saint-Omer,17005,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2258 +Sainte-Thècle,35050,Mauricie,condo,1.0,1,0.3,not_published
2259 +Notre-Dame-de-Lorette,92060,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
2260 +Estérel,77011,Laurentides,plex,1.0,1,0.3,not_published
2261 +TNO aquatique de la MRC de Pontiac,84990,Outaouais,plex,1.0,1,0.3,not_published
2262 +Marston,30035,Estrie,plex,1.0,1,0.3,not_published
2263 +Milan,30040,Estrie,plex,1.0,1,0.3,not_published
2264 +Preissac,88090,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2265 +Saint-Mathieu-d'Harricana,88050,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2266 +Clermont,87110,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2267 +Calixa-Lavallée,59030,Montérégie,plex,1.0,1,0.3,not_published
2268 +Saint-Adalbert,17015,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2269 +Irlande,31040,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2270 +Auclair,13045,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2271 +Saint-Narcisse-de-Beaurivage,33030,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2272 +Saint-Julien,31035,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2273 +Sainte-Françoise,38035,Centre-du-Québec,plex,1.0,1,0.3,not_published
2274 +Ogden,45020,Estrie,plex,1.0,1,0.3,not_published
2275 +Grosse-Île,01042,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2276 +Lac-Chicobi,88904,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2277 +Nominingue,79030,Laurentides,condo,1.0,1,0.3,not_published
2278 +Kipawa,85010,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2279 +Lac-Walker,97904,Côte-Nord,plex,1.0,1,0.3,not_published
2280 +Kinnear's Mills,31105,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2281 +Saint-Rosaire,39145,Centre-du-Québec,plex,1.0,1,0.3,not_published
2282 +Saint-André-de-Restigouche,06040,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2283 +Grand-Métis,09060,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2284 +Saint-Eusèbe,13030,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2285 +Rivière-aux-Outardes,96902,Côte-Nord,plex,1.0,1,0.3,not_published
2286 +Saint-Séverin,27070,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2287 +Laniel,85905,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2288 +Cloridorme,03010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2289 +Maricourt,42065,Estrie,plex,1.0,1,0.3,not_published
2290 +Denholm,83005,Outaouais,plex,1.0,1,0.3,not_published
2291 +Saint-Léon-le-Grand,51035,Mauricie,plex,1.0,1,0.3,not_published
2292 +Saint-Paul-de-la-Croix,12035,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2293 +TNO aquatique de la MRC du Rocher-Percé,02990,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2294 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,condo,1.0,1,0.3,not_published
2295 +Sainte-Élisabeth-de-Proulx,92903,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
2296 +Saint-Joseph-de-Kamouraska,14030,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2297 +Beaulac-Garthby,31008,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2298 +La Trinité-des-Monts,10010,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2299 +Saint-Alexis,63023,Lanaudière,condo,1.0,1,0.3,not_published
2300 +Saint-Anselme,19062,Chaudière-Appalaches,condo,1.0,1,0.3,not_published
2301 +Saint-Stanislas,92070,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
2302 +Saint-Arsène,12065,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2303 +Bedford,46040,Estrie,plex,1.0,1,0.3,not_published
2304 +Saint-Jules,27055,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2305 +Hampden,41075,Estrie,plex,1.0,1,0.3,not_published
2306 +Saint-Alphonse,05065,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,0.3,not_published
2307 +Baie-Sainte-Catherine,15065,Capitale-Nationale,plex,1.0,1,0.3,not_published
2308 +Pointe-Calumet,72020,Laurentides,condo,1.0,1,0.3,not_published
2309 +Saint-Marcel-de-Richelieu,54125,Montérégie,plex,1.0,1,0.3,not_published
2310 +La Motte,88045,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2311 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,condo,1.0,1,0.3,not_published
2312 +Ferland-et-Boilleau,94220,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
2313 +Portage-du-Fort,84020,Outaouais,plex,1.0,1,0.3,not_published
2314 +Côte-Nord-du-Golfe-du-Saint-Laurent,98015,Côte-Nord,plex,1.0,1,0.3,not_published
2315 +Martinville,44060,Estrie,plex,1.0,1,0.3,not_published
2316 +Lac-Édouard,90027,Mauricie,plex,1.0,1,0.3,not_published
2317 +Saint-Gilbert,34060,Capitale-Nationale,plex,1.0,1,0.3,not_published
2318 +Ulverton,42078,Estrie,plex,1.0,1,0.3,not_published
2319 +Saint-Gabriel-Lalemant,14075,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2320 +Saint-Louis-de-Blandford,39170,Centre-du-Québec,plex,1.0,1,0.3,not_published
2321 +Saint-Basile,34038,Capitale-Nationale,condo,1.0,1,0.3,not_published
2322 +Sainte-Sabine,28065,Chaudière-Appalaches,plex,1.0,1,0.3,not_published
2323 +Saint-Bernard-de-Michaudville,54115,Montérégie,plex,1.0,1,0.3,not_published
2324 +Sainte-Marguerite-Marie,07005,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2325 +Saint-Louis,54120,Montérégie,plex,1.0,1,0.3,not_published
2326 +Sainte-Famille-de-l'Île-d'Orléans,20010,Capitale-Nationale,plex,1.0,1,0.3,not_published
2327 +Sainte-Clotilde-de-Horton,39117,Centre-du-Québec,plex,1.0,1,0.3,not_published
2328 +Mont-Élie,15902,Capitale-Nationale,plex,1.0,1,0.3,not_published
2329 +Val-Racine,30015,Estrie,plex,1.0,1,0.3,not_published
2330 +La Rédemption,09005,Bas-Saint-Laurent,plex,1.0,1,0.3,not_published
2331 +Baie-Trinité,96005,Côte-Nord,plex,1.0,1,0.3,not_published
2332 +Belle-Rivière,93908,Saguenay–Lac-Saint-Jean,unifamilial,1.0,1,0.3,not_published
2333 +Guérin,85095,Abitibi-Témiscamingue,plex,1.0,1,0.3,not_published
2334 +Saint-Télesphore,71015,Montérégie,plex,1.0,1,0.3,not_published
2335 +Saint-Edmond-les-Plaines,92050,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
2336 +Saint-Eugène-d'Argentenay,92065,Saguenay–Lac-Saint-Jean,plex,1.0,1,0.3,not_published
added outputs/tables/coverage_monthly.csv +2336 −0
@@ -0,0 +1,2336 @@
1 +municipality,geo_code,region,propertyType,median_monthly_tx,total_tx,pct_months_active,coverage_status
2 +Montréal,66023,Montréal,condo,620.0,44539,100.0,published
3 +Montréal,66023,Montréal,plex,346.0,24009,100.0,published
4 +Québec,23027,Capitale-Nationale,unifamilial,303.0,23614,100.0,published
5 +Montréal,66023,Montréal,unifamilial,280.0,19257,100.0,published
6 +Gatineau,81017,Outaouais,unifamilial,240.0,17261,100.0,published
7 +Laval,65005,Laval,unifamilial,234.0,16780,100.0,published
8 +Québec,23027,Capitale-Nationale,condo,173.0,13156,100.0,published
9 +Sherbrooke,43027,Estrie,unifamilial,118.0,9253,100.0,published
10 +Lévis,25213,Chaudière-Appalaches,unifamilial,111.0,9231,100.0,published
11 +Saguenay,94068,Saguenay–Lac-Saint-Jean,unifamilial,117.0,8761,100.0,published
12 +Longueuil,58227,Montérégie,unifamilial,118.0,8747,100.0,published
13 +Trois-Rivières,37067,Mauricie,unifamilial,101.0,7325,100.0,published
14 +Terrebonne,64008,Lanaudière,unifamilial,93.0,7143,100.0,conditional
15 +Québec,23027,Capitale-Nationale,plex,92.0,6577,100.0,published
16 +Laval,65005,Laval,condo,91.0,6245,100.0,published
17 +Saint-Jean-sur-Richelieu,56083,Montérégie,unifamilial,75.0,5807,100.0,conditional
18 +Drummondville,49058,Centre-du-Québec,unifamilial,72.0,5104,100.0,published
19 +Repentigny,60013,Lanaudière,unifamilial,68.0,5091,100.0,conditional
20 +Longueuil,58227,Montérégie,condo,60.0,4628,100.0,published
21 +Saint-Jérôme,75017,Laurentides,unifamilial,61.0,4503,100.0,conditional
22 +Granby,47017,Estrie,unifamilial,52.0,4024,100.0,conditional
23 +Blainville,73015,Laurentides,unifamilial,50.0,3733,100.0,conditional
24 +Mirabel,74005,Laurentides,unifamilial,51.0,3705,100.0,conditional
25 +Gatineau,81017,Outaouais,plex,52.0,3660,100.0,published
26 +Brossard,58007,Montérégie,unifamilial,51.0,3475,100.0,conditional
27 +Shawinigan,36033,Mauricie,unifamilial,45.0,3346,100.0,conditional
28 +Mascouche,64015,Lanaudière,unifamilial,44.0,3214,100.0,conditional
29 +Rimouski,10043,Bas-Saint-Laurent,unifamilial,41.0,3048,100.0,conditional
30 +Victoriaville,39062,Centre-du-Québec,unifamilial,40.0,2996,100.0,conditional
31 +Châteauguay,67050,Montérégie,unifamilial,41.0,2894,100.0,conditional
32 +Salaberry-de-Valleyfield,70052,Montérégie,unifamilial,38.0,2882,100.0,conditional
33 +Longueuil,58227,Montérégie,plex,40.0,2741,100.0,published
34 +Saint-Hyacinthe,54048,Montérégie,unifamilial,34.0,2727,100.0,conditional
35 +Saint-Eustache,72005,Laurentides,unifamilial,34.0,2576,100.0,conditional
36 +Saguenay,94068,Saguenay–Lac-Saint-Jean,plex,37.0,2576,100.0,published
37 +Boucherville,58033,Montérégie,unifamilial,35.0,2559,100.0,conditional
38 +Trois-Rivières,37067,Mauricie,plex,34.0,2535,100.0,published
39 +Gatineau,81017,Outaouais,condo,30.0,2519,100.0,published
40 +Vaudreuil-Dorion,71083,Montérégie,unifamilial,33.0,2465,100.0,conditional
41 +Laval,65005,Laval,plex,34.0,2434,100.0,published
42 +Sherbrooke,43027,Estrie,plex,34.0,2402,100.0,published
43 +Sorel-Tracy,53052,Montérégie,unifamilial,30.0,2353,100.0,conditional
44 +Saint-Lin–Laurentides,63048,Lanaudière,unifamilial,32.0,2329,100.0,conditional
45 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,unifamilial,32.0,2327,100.0,conditional
46 +Val-d'Or,89008,Abitibi-Témiscamingue,unifamilial,33.0,2194,100.0,conditional
47 +Saint-Georges,29073,Chaudière-Appalaches,unifamilial,25.0,2019,100.0,conditional
48 +Magog,45072,Estrie,unifamilial,27.0,2010,100.0,conditional
49 +Alma,93042,Saguenay–Lac-Saint-Jean,unifamilial,26.0,1975,100.0,conditional
50 +Chambly,57005,Montérégie,unifamilial,26.0,1952,100.0,conditional
51 +Sainte-Julie,59010,Montérégie,unifamilial,25.0,1943,100.0,conditional
52 +Dollard-des-Ormeaux,66142,Montréal,unifamilial,26.0,1872,100.0,conditional
53 +Brossard,58007,Montérégie,condo,17.0,1797,100.0,conditional
54 +Thetford Mines,31084,Chaudière-Appalaches,unifamilial,24.0,1773,100.0,conditional
55 +Saint-Constant,67035,Montérégie,unifamilial,23.0,1708,100.0,conditional
56 +Saint-Bruno-de-Montarville,58037,Montérégie,unifamilial,24.0,1702,100.0,conditional
57 +Beloeil,57040,Montérégie,unifamilial,22.0,1697,100.0,conditional
58 +Saint-Jérôme,75017,Laurentides,plex,24.0,1696,100.0,conditional
59 +Repentigny,60013,Lanaudière,condo,22.0,1688,100.0,conditional
60 +Sainte-Sophie,75028,Laurentides,unifamilial,24.0,1685,100.0,conditional
61 +L'Assomption,60028,Lanaudière,unifamilial,21.0,1600,100.0,conditional
62 +Saint-Colomban,75005,Laurentides,unifamilial,21.0,1568,100.0,conditional
63 +Sept-Îles,97007,Côte-Nord,unifamilial,24.0,1563,100.0,conditional
64 +Saint-Hippolyte,75045,Laurentides,unifamilial,22.0,1558,100.0,conditional
65 +Candiac,67020,Montérégie,unifamilial,21.0,1555,100.0,conditional
66 +Sainte-Adèle,77022,Laurentides,unifamilial,21.0,1534,100.0,conditional
67 +Saint-Lazare,71105,Montérégie,unifamilial,21.0,1521,100.0,conditional
68 +Boisbriand,73005,Laurentides,unifamilial,20.0,1519,100.0,conditional
69 +Lévis,25213,Chaudière-Appalaches,plex,22.0,1511,100.0,published
70 +Shawinigan,36033,Mauricie,plex,20.0,1485,100.0,conditional
71 +Sainte-Julienne,63060,Lanaudière,unifamilial,20.0,1468,100.0,conditional
72 +La Prairie,67015,Montérégie,unifamilial,20.0,1465,100.0,conditional
73 +Saint-Sauveur,77043,Laurentides,unifamilial,21.0,1461,100.0,conditional
74 +Mont-Saint-Hilaire,57035,Montérégie,unifamilial,20.0,1428,100.0,conditional
75 +Val-des-Monts,82015,Outaouais,unifamilial,20.0,1424,100.0,conditional
76 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,unifamilial,18.0,1399,100.0,conditional
77 +Lavaltrie,52007,Lanaudière,unifamilial,18.0,1393,100.0,conditional
78 +Rawdon,62037,Lanaudière,unifamilial,18.0,1366,100.0,conditional
79 +Pointe-Claire,66097,Montréal,unifamilial,18.0,1340,100.0,conditional
80 +Varennes,59020,Montérégie,unifamilial,18.0,1312,100.0,conditional
81 +Drummondville,49058,Centre-du-Québec,plex,16.0,1205,100.0,published
82 +Prévost,75040,Laurentides,unifamilial,17.0,1203,100.0,conditional
83 +Baie-Comeau,96020,Côte-Nord,unifamilial,16.0,1179,100.0,conditional
84 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,unifamilial,14.0,1162,100.0,not_published
85 +Saint-Basile-le-Grand,57020,Montérégie,unifamilial,15.0,1148,100.0,conditional
86 +Beaconsfield,66107,Montréal,unifamilial,17.0,1147,100.0,conditional
87 +Mirabel,74005,Laurentides,plex,16.0,1146,100.0,conditional
88 +Mercier,67045,Montérégie,unifamilial,15.0,1141,100.0,conditional
89 +Saint-Raymond,34128,Capitale-Nationale,unifamilial,15.0,1140,100.0,conditional
90 +Saint-Calixte,63055,Lanaudière,unifamilial,16.0,1130,100.0,conditional
91 +Bromont,46078,Estrie,unifamilial,15.0,1053,100.0,conditional
92 +Sainte-Anne-des-Plaines,73035,Laurentides,unifamilial,13.0,1049,100.0,not_published
93 +L'Ancienne-Lorette,23057,Capitale-Nationale,unifamilial,13.0,1049,100.0,not_published
94 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,unifamilial,13.0,1043,98.5,not_published
95 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,unifamilial,14.0,1039,100.0,not_published
96 +Saint-Amable,59015,Montérégie,unifamilial,13.0,1039,100.0,not_published
97 +Saint-Jean-sur-Richelieu,56083,Montérégie,plex,14.0,1029,100.0,not_published
98 +Chertsey,62047,Lanaudière,unifamilial,15.0,1026,100.0,conditional
99 +Dolbeau-Mistassini,92022,Saguenay–Lac-Saint-Jean,unifamilial,14.0,1023,100.0,not_published
100 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,unifamilial,12.0,1015,100.0,not_published
101 +Deux-Montagnes,72010,Laurentides,unifamilial,13.0,1010,100.0,not_published
102 +Bécancour,38010,Centre-du-Québec,unifamilial,14.0,1010,100.0,not_published
103 +Sainte-Catherine,67030,Montérégie,unifamilial,13.0,1004,100.0,not_published
104 +Lachute,76020,Laurentides,unifamilial,14.0,1001,100.0,not_published
105 +Rivière-du-Loup,12072,Bas-Saint-Laurent,unifamilial,12.0,990,98.5,not_published
106 +Sainte-Thérèse,73010,Laurentides,unifamilial,13.0,981,100.0,not_published
107 +Saint-Apollinaire,33090,Chaudière-Appalaches,unifamilial,12.0,970,98.5,not_published
108 +Rosemère,73020,Laurentides,unifamilial,13.0,970,100.0,not_published
109 +Cowansville,46080,Estrie,unifamilial,13.0,968,100.0,not_published
110 +Mont-Tremblant,78102,Laurentides,unifamilial,14.0,968,100.0,not_published
111 +Salaberry-de-Valleyfield,70052,Montérégie,plex,13.0,955,100.0,not_published
112 +Saint-Lambert,58012,Montérégie,unifamilial,14.0,947,100.0,not_published
113 +Granby,47017,Estrie,plex,12.0,944,100.0,not_published
114 +Terrebonne,64008,Lanaudière,condo,12.0,929,100.0,not_published
115 +Terrebonne,64008,Lanaudière,plex,12.0,920,100.0,not_published
116 +Lévis,25213,Chaudière-Appalaches,condo,9.5,919,98.5,published
117 +Pont-Rouge,34017,Capitale-Nationale,unifamilial,11.0,918,98.5,not_published
118 +Matane,08053,Bas-Saint-Laurent,unifamilial,13.0,917,100.0,not_published
119 +Kirkland,66102,Montréal,unifamilial,12.0,917,100.0,not_published
120 +Carignan,57010,Montérégie,unifamilial,12.0,912,100.0,not_published
121 +Beauharnois,70022,Montérégie,unifamilial,12.0,906,100.0,not_published
122 +Mont-Laurier,79088,Laurentides,unifamilial,12.0,904,100.0,not_published
123 +Contrecoeur,59035,Montérégie,unifamilial,12.0,884,98.5,not_published
124 +Sainte-Agathe-des-Monts,78032,Laurentides,unifamilial,13.0,882,100.0,not_published
125 +Cantley,82020,Outaouais,unifamilial,12.0,878,100.0,not_published
126 +Saint-Adolphe-d'Howard,77065,Laurentides,unifamilial,13.0,865,98.5,not_published
127 +Saint-Zotique,71025,Montérégie,unifamilial,12.0,855,100.0,not_published
128 +Pincourt,71070,Montérégie,unifamilial,12.0,852,100.0,not_published
129 +Saint-Donat,62060,Lanaudière,unifamilial,12.0,846,100.0,not_published
130 +Marieville,55048,Montérégie,unifamilial,10.0,834,100.0,not_published
131 +Joliette,61025,Lanaudière,unifamilial,11.0,827,100.0,not_published
132 +Chelsea,82025,Outaouais,unifamilial,11.0,809,98.5,not_published
133 +Sainte-Marie,26030,Chaudière-Appalaches,unifamilial,10.0,808,100.0,not_published
134 +La Pêche,82035,Outaouais,unifamilial,11.0,807,100.0,not_published
135 +Saint-Hyacinthe,54048,Montérégie,plex,11.0,800,100.0,not_published
136 +Lorraine,73025,Laurentides,unifamilial,11.0,796,100.0,not_published
137 +Les Îles-de-la-Madeleine,01023,Gaspésie–Îles-de-la-Madeleine,unifamilial,11.0,778,100.0,not_published
138 +Westmount,66032,Montréal,unifamilial,10.0,765,100.0,not_published
139 +Saint-Jérôme,75017,Laurentides,condo,7.0,760,98.5,not_published
140 +Brownsburg-Chatham,76043,Laurentides,unifamilial,12.0,756,100.0,not_published
141 +Saint-Félicien,91042,Saguenay–Lac-Saint-Jean,unifamilial,11.0,755,100.0,not_published
142 +Lac-Beauport,22040,Capitale-Nationale,unifamilial,10.0,751,97.0,not_published
143 +Saint-Philippe,67010,Montérégie,unifamilial,10.0,748,98.5,not_published
144 +Otterburn Park,57030,Montérégie,unifamilial,10.0,745,100.0,not_published
145 +Gaspé,03005,Gaspésie–Îles-de-la-Madeleine,unifamilial,10.0,742,100.0,not_published
146 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,unifamilial,11.0,741,100.0,not_published
147 +Farnham,46112,Estrie,unifamilial,10.0,719,100.0,not_published
148 +La Tuque,90012,Mauricie,unifamilial,11.0,714,98.5,not_published
149 +Mascouche,64015,Lanaudière,condo,7.0,713,100.0,not_published
150 +Mont-Royal,66072,Montréal,unifamilial,11.0,711,98.5,not_published
151 +Orford,45115,Estrie,unifamilial,10.0,703,100.0,not_published
152 +Notre-Dame-des-Prairies,61030,Lanaudière,unifamilial,10.0,691,100.0,not_published
153 +Côte-Saint-Luc,66058,Montréal,condo,10.0,681,100.0,not_published
154 +Sorel-Tracy,53052,Montérégie,plex,10.0,670,100.0,not_published
155 +Côte-Saint-Luc,66058,Montréal,unifamilial,9.0,668,100.0,not_published
156 +Chibougamau,99025,Nord-du-Québec,unifamilial,8.5,662,98.5,not_published
157 +Dorval,66087,Montréal,unifamilial,9.0,660,98.5,not_published
158 +L'Épiphanie,60037,Lanaudière,unifamilial,9.0,651,98.5,not_published
159 +Saint-Charles-Borromée,61035,Lanaudière,unifamilial,9.0,645,100.0,not_published
160 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,unifamilial,9.0,643,100.0,not_published
161 +Boischatel,21045,Capitale-Nationale,unifamilial,7.0,638,98.5,not_published
162 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,plex,9.0,633,100.0,not_published
163 +Bois-des-Filion,73030,Laurentides,unifamilial,9.0,631,100.0,not_published
164 +Roberval,91025,Saguenay–Lac-Saint-Jean,unifamilial,8.0,625,100.0,not_published
165 +Shefford,47035,Estrie,unifamilial,9.0,624,100.0,not_published
166 +Lac-Brome,46075,Estrie,unifamilial,9.0,619,100.0,not_published
167 +Morin-Heights,77050,Laurentides,unifamilial,9.0,612,100.0,not_published
168 +Rigaud,71133,Montérégie,unifamilial,9.0,609,100.0,not_published
169 +Montmagny,18050,Chaudière-Appalaches,unifamilial,8.0,605,100.0,not_published
170 +La Malbaie,15013,Capitale-Nationale,unifamilial,8.0,596,100.0,not_published
171 +Saint-Jean-de-Matha,62015,Lanaudière,unifamilial,9.0,594,98.5,not_published
172 +Donnacona,34025,Capitale-Nationale,unifamilial,7.5,588,98.5,not_published
173 +Shannon,22020,Capitale-Nationale,unifamilial,7.0,587,97.0,not_published
174 +Acton Vale,48028,Montérégie,unifamilial,7.0,586,100.0,not_published
175 +Blainville,73015,Laurentides,plex,8.0,576,100.0,not_published
176 +Saint-Lambert,58012,Montérégie,condo,7.0,574,97.0,not_published
177 +Saint-Honoré,94240,Saguenay–Lac-Saint-Jean,unifamilial,8.0,570,100.0,not_published
178 +Boucherville,58033,Montérégie,condo,7.0,570,97.0,not_published
179 +Pointe-Calumet,72020,Laurentides,unifamilial,8.0,568,100.0,not_published
180 +Val-David,78010,Laurentides,unifamilial,9.0,566,100.0,not_published
181 +Hébertville,93022,Saguenay–Lac-Saint-Jean,unifamilial,8.0,561,100.0,not_published
182 +Saint-Paul,61005,Lanaudière,unifamilial,8.0,555,100.0,not_published
183 +Alma,93042,Saguenay–Lac-Saint-Jean,plex,7.5,554,98.5,not_published
184 +Saint-Alphonse-Rodriguez,62025,Lanaudière,unifamilial,7.0,554,100.0,not_published
185 +Val-des-Sources,40043,Estrie,unifamilial,8.0,547,98.5,not_published
186 +Coaticook,44037,Estrie,unifamilial,8.0,546,100.0,not_published
187 +Waterloo,47025,Estrie,unifamilial,8.0,544,100.0,not_published
188 +L'Île-Perrot,71060,Montérégie,unifamilial,8.0,542,98.5,not_published
189 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,condo,7.0,538,98.5,not_published
190 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,unifamilial,8.0,538,100.0,not_published
191 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,unifamilial,8.0,537,94.0,not_published
192 +Saint-Félix-de-Valois,62007,Lanaudière,unifamilial,8.0,532,98.5,not_published
193 +Victoriaville,39062,Centre-du-Québec,plex,8.0,526,100.0,not_published
194 +Hudson,71100,Montérégie,unifamilial,7.0,520,97.0,not_published
195 +L'Ange-Gardien,82005,Outaouais,unifamilial,7.0,515,98.5,not_published
196 +Pointe-Claire,66097,Montréal,condo,6.0,508,98.5,not_published
197 +Roxton Pond,47047,Estrie,unifamilial,7.0,504,100.0,not_published
198 +Coteau-du-Lac,71040,Montérégie,unifamilial,7.0,503,98.5,not_published
199 +Sherbrooke,43027,Estrie,condo,7.0,498,100.0,published
200 +Delson,67025,Montérégie,unifamilial,6.0,494,98.5,not_published
201 +Les Cèdres,71050,Montérégie,unifamilial,7.0,494,97.0,not_published
202 +Lanoraie,52017,Lanaudière,unifamilial,7.0,493,100.0,not_published
203 +Magog,45072,Estrie,condo,7.0,493,98.5,not_published
204 +Chambly,57005,Montérégie,condo,6.0,491,95.5,not_published
205 +Sainte-Thérèse,73010,Laurentides,plex,7.0,490,98.5,not_published
206 +Saint-Rémi,68055,Montérégie,unifamilial,7.0,490,98.5,not_published
207 +Saint-Côme,62065,Lanaudière,unifamilial,7.0,488,100.0,not_published
208 +Saint-Jean-sur-Richelieu,56083,Montérégie,condo,6.0,488,95.5,not_published
209 +Mascouche,64015,Lanaudière,plex,7.0,486,100.0,not_published
210 +Saint-Pie,54008,Montérégie,unifamilial,5.5,484,98.5,not_published
211 +Saint-Lin–Laurentides,63048,Lanaudière,plex,7.0,483,100.0,not_published
212 +Sainte-Anne-des-Lacs,77035,Laurentides,unifamilial,6.0,481,100.0,not_published
213 +Notre-Dame-du-Mont-Carmel,37235,Mauricie,unifamilial,6.0,480,100.0,not_published
214 +Mont-Tremblant,78102,Laurentides,condo,6.0,478,100.0,not_published
215 +Thetford Mines,31084,Chaudière-Appalaches,plex,7.0,476,100.0,not_published
216 +Saint-Damien,62075,Lanaudière,unifamilial,7.0,474,100.0,not_published
217 +Rivière-Rouge,79037,Laurentides,unifamilial,7.0,472,98.5,not_published
218 +Nicolet,50072,Centre-du-Québec,unifamilial,6.0,472,98.5,not_published
219 +Mont-Blanc,78047,Laurentides,unifamilial,6.0,468,98.5,not_published
220 +Sainte-Anne-des-Monts,04037,Gaspésie–Îles-de-la-Madeleine,unifamilial,7.0,464,98.5,not_published
221 +Les Coteaux,71033,Montérégie,unifamilial,6.0,462,100.0,not_published
222 +Piedmont,77030,Laurentides,unifamilial,6.0,455,100.0,not_published
223 +Saint-Denis-de-Brompton,42025,Estrie,unifamilial,6.0,454,98.5,not_published
224 +Pontiac,82030,Outaouais,unifamilial,6.0,446,100.0,not_published
225 +Saint-Alexis-des-Monts,51065,Mauricie,unifamilial,6.0,445,100.0,not_published
226 +Granby,47017,Estrie,condo,6.0,442,95.5,not_published
227 +Dollard-des-Ormeaux,66142,Montréal,condo,7.0,439,97.0,not_published
228 +Val-d'Or,89008,Abitibi-Témiscamingue,plex,6.0,431,100.0,not_published
229 +Saint-Agapit,33045,Chaudière-Appalaches,unifamilial,6.0,431,98.5,not_published
230 +Sainte-Sophie,75028,Laurentides,plex,6.5,430,98.5,not_published
231 +Dorval,66087,Montréal,condo,6.0,424,100.0,not_published
232 +La Sarre,87090,Abitibi-Témiscamingue,unifamilial,6.0,424,97.0,not_published
233 +Sutton,46058,Estrie,unifamilial,6.0,415,98.5,not_published
234 +Mont-Joli,09077,Bas-Saint-Laurent,unifamilial,6.0,415,97.0,not_published
235 +Repentigny,60013,Lanaudière,plex,5.0,412,98.5,not_published
236 +Beaupré,21025,Capitale-Nationale,unifamilial,5.0,408,98.5,not_published
237 +Saint-Henri,19068,Chaudière-Appalaches,unifamilial,5.0,406,97.0,not_published
238 +Joliette,61025,Lanaudière,plex,5.0,404,100.0,not_published
239 +Port-Cartier,97022,Côte-Nord,unifamilial,6.0,400,95.5,not_published
240 +Princeville,32033,Centre-du-Québec,unifamilial,6.0,398,100.0,not_published
241 +Verchères,59025,Montérégie,unifamilial,5.0,398,97.0,not_published
242 +Château-Richer,21035,Capitale-Nationale,unifamilial,5.0,396,97.0,not_published
243 +Rimouski,10043,Bas-Saint-Laurent,plex,5.0,396,100.0,not_published
244 +Louiseville,51015,Mauricie,unifamilial,5.0,391,97.0,not_published
245 +Saint-Césaire,55023,Montérégie,unifamilial,5.0,391,95.5,not_published
246 +Mandeville,52095,Lanaudière,unifamilial,5.0,381,98.5,not_published
247 +Sainte-Thérèse,73010,Laurentides,condo,4.0,380,94.0,not_published
248 +Eastman,45093,Estrie,unifamilial,5.0,378,100.0,not_published
249 +Gore,76025,Laurentides,unifamilial,5.0,378,100.0,not_published
250 +Trois-Rivières,37067,Mauricie,condo,4.0,376,95.5,published
251 +Saint-Roch-de-Richelieu,53040,Montérégie,unifamilial,5.0,375,97.0,not_published
252 +Saint-Joseph-du-Lac,72025,Laurentides,unifamilial,5.0,374,98.5,not_published
253 +Magog,45072,Estrie,plex,5.0,372,97.0,not_published
254 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,unifamilial,6.0,371,97.0,not_published
255 +Val-Morin,78005,Laurentides,unifamilial,5.0,371,100.0,not_published
256 +Lac-Supérieur,78095,Laurentides,unifamilial,5.0,370,98.5,not_published
257 +Notre-Dame-du-Laus,79005,Laurentides,unifamilial,5.0,368,97.0,not_published
258 +Lac-Etchemin,28053,Chaudière-Appalaches,unifamilial,5.0,365,97.0,not_published
259 +Saint-Michel-des-Saints,62085,Lanaudière,unifamilial,6.0,365,97.0,not_published
260 +Saint-Eustache,72005,Laurentides,condo,4.0,365,92.5,not_published
261 +Baie-Saint-Paul,16013,Capitale-Nationale,unifamilial,5.0,364,94.0,not_published
262 +Saint-Roch-de-l'Achigan,63035,Lanaudière,unifamilial,5.0,362,98.5,not_published
263 +Sainte-Adèle,77022,Laurentides,plex,5.0,361,98.5,not_published
264 +Chandler,02028,Gaspésie–Îles-de-la-Madeleine,unifamilial,5.0,357,98.5,not_published
265 +Saint-Anicet,69070,Montérégie,unifamilial,5.0,354,98.5,not_published
266 +Grenville-sur-la-Rouge,76052,Laurentides,unifamilial,5.0,351,97.0,not_published
267 +Wentworth-Nord,77060,Laurentides,unifamilial,5.0,347,98.5,not_published
268 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,unifamilial,5.0,344,92.5,not_published
269 +Neuville,34007,Capitale-Nationale,unifamilial,4.0,343,94.0,not_published
270 +Sainte-Agathe-des-Monts,78032,Laurentides,plex,5.0,340,97.0,not_published
271 +Austin,45085,Estrie,unifamilial,5.0,339,97.0,not_published
272 +Thurso,80050,Outaouais,unifamilial,4.0,336,98.5,not_published
273 +Saint-Jacques,63013,Lanaudière,unifamilial,4.0,335,97.0,not_published
274 +Labelle,78120,Laurentides,unifamilial,4.0,334,98.5,not_published
275 +Lac-Mégantic,30030,Estrie,unifamilial,4.0,333,100.0,not_published
276 +Saint-Cyrille-de-Wendover,49070,Centre-du-Québec,unifamilial,4.0,329,97.0,not_published
277 +Lachute,76020,Laurentides,plex,4.5,328,98.5,not_published
278 +Venise-en-Québec,56005,Montérégie,unifamilial,5.0,327,97.0,not_published
279 +Saint-André-Avellin,80027,Outaouais,unifamilial,4.0,327,100.0,not_published
280 +Gracefield,83032,Outaouais,unifamilial,5.0,326,97.0,not_published
281 +Saint-Boniface,51085,Mauricie,unifamilial,5.0,325,95.5,not_published
282 +La Pocatière,14082,Bas-Saint-Laurent,unifamilial,4.0,325,98.5,not_published
283 +Napierville,68030,Montérégie,unifamilial,4.0,325,98.5,not_published
284 +Saguenay,94068,Saguenay–Lac-Saint-Jean,condo,5.0,321,89.6,published
285 +Saint-Gabriel-de-Brandon,52085,Lanaudière,unifamilial,4.0,321,98.5,not_published
286 +Amqui,07047,Bas-Saint-Laurent,unifamilial,4.0,321,94.0,not_published
287 +Nominingue,79030,Laurentides,unifamilial,4.0,321,98.5,not_published
288 +Oka,72032,Laurentides,unifamilial,5.0,320,95.5,not_published
289 +Saint-Eustache,72005,Laurentides,plex,4.0,320,98.5,not_published
290 +Danville,40047,Estrie,unifamilial,4.0,319,98.5,not_published
291 +Sainte-Julie,59010,Montérégie,condo,4.0,318,95.5,not_published
292 +Saint-Ambroise,94255,Saguenay–Lac-Saint-Jean,unifamilial,4.5,318,92.5,not_published
293 +Potton,45030,Estrie,unifamilial,5.0,317,100.0,not_published
294 +Candiac,67020,Montérégie,condo,4.0,314,91.0,not_published
295 +Beauceville,27028,Chaudière-Appalaches,unifamilial,4.0,314,95.5,not_published
296 +Weedon,41098,Estrie,unifamilial,4.0,314,98.5,not_published
297 +Amherst,78070,Laurentides,unifamilial,4.0,313,97.0,not_published
298 +Sainte-Béatrix,62020,Lanaudière,unifamilial,5.0,312,97.0,not_published
299 +Cap-Santé,34030,Capitale-Nationale,unifamilial,4.0,312,92.5,not_published
300 +Saint-Mathias-sur-Richelieu,55065,Montérégie,unifamilial,4.0,311,97.0,not_published
301 +Portneuf,34048,Capitale-Nationale,unifamilial,4.0,310,97.0,not_published
302 +Saint-Gilles,33035,Chaudière-Appalaches,unifamilial,5.0,310,94.0,not_published
303 +Saint-Étienne-des-Grès,51090,Mauricie,unifamilial,4.0,309,97.0,not_published
304 +Saint-Tite,35027,Mauricie,unifamilial,4.0,308,100.0,not_published
305 +Windsor,42088,Estrie,unifamilial,4.0,307,94.0,not_published
306 +Saint-Bruno-de-Montarville,58037,Montérégie,condo,4.0,306,97.0,not_published
307 +Sainte-Martine,70012,Montérégie,unifamilial,4.0,302,97.0,not_published
308 +Métabetchouan–Lac-à-la-Croix,93012,Saguenay–Lac-Saint-Jean,unifamilial,4.0,301,94.0,not_published
309 +Plessisville,32043,Centre-du-Québec,unifamilial,4.0,299,85.1,not_published
310 +Saint-Anselme,19062,Chaudière-Appalaches,unifamilial,4.0,299,94.0,not_published
311 +Cookshire-Eaton,41038,Estrie,unifamilial,4.0,298,95.5,not_published
312 +Saint-Paul-de-l'Île-aux-Noix,56035,Montérégie,unifamilial,4.0,298,97.0,not_published
313 +Maniwaki,83065,Outaouais,unifamilial,4.0,291,94.0,not_published
314 +Richelieu,55057,Montérégie,unifamilial,4.0,289,95.5,not_published
315 +Saint-Germain-de-Grantham,49048,Centre-du-Québec,unifamilial,4.0,288,98.5,not_published
316 +East Angus,41060,Estrie,unifamilial,4.0,288,94.0,not_published
317 +Lac-Simon,80095,Outaouais,unifamilial,4.0,284,95.5,not_published
318 +Saint-Joseph-de-Beauce,27043,Chaudière-Appalaches,unifamilial,4.0,282,97.0,not_published
319 +Beaumont,19105,Chaudière-Appalaches,unifamilial,4.0,280,91.0,not_published
320 +Fossambault-sur-le-Lac,22010,Capitale-Nationale,unifamilial,4.0,280,94.0,not_published
321 +Dunham,46050,Estrie,unifamilial,4.0,279,98.5,not_published
322 +Saint-Prosper,28020,Chaudière-Appalaches,unifamilial,4.0,279,97.0,not_published
323 +Saint-Georges,29073,Chaudière-Appalaches,plex,4.0,279,97.0,not_published
324 +McMasterville,57025,Montérégie,unifamilial,4.0,278,94.0,not_published
325 +Saint-Antonin,12015,Bas-Saint-Laurent,unifamilial,3.0,275,94.0,not_published
326 +Saint-André-d'Argenteuil,76008,Laurentides,unifamilial,4.0,275,95.5,not_published
327 +Lac-des-Écorces,79078,Laurentides,unifamilial,4.0,273,97.0,not_published
328 +Sainte-Émélie-de-l'Énergie,62070,Lanaudière,unifamilial,4.0,273,91.0,not_published
329 +Adstock,31056,Chaudière-Appalaches,unifamilial,4.0,272,95.5,not_published
330 +Baie-Comeau,96020,Côte-Nord,plex,4.0,272,94.0,not_published
331 +Châteauguay,67050,Montérégie,plex,4.0,271,94.0,not_published
332 +Sainte-Mélanie,61050,Lanaudière,unifamilial,4.0,270,92.5,not_published
333 +Notre-Dame-de-la-Merci,62055,Lanaudière,unifamilial,4.0,267,98.5,not_published
334 +La Minerve,78130,Laurentides,unifamilial,4.0,267,95.5,not_published
335 +Warwick,39077,Centre-du-Québec,unifamilial,3.0,265,98.5,not_published
336 +Beloeil,57040,Montérégie,condo,4.0,264,83.6,not_published
337 +Sainte-Claire,19055,Chaudière-Appalaches,unifamilial,3.0,264,88.1,not_published
338 +Saint-Sauveur,77043,Laurentides,plex,4.0,263,95.5,not_published
339 +Huntingdon,69055,Montérégie,unifamilial,4.0,262,94.0,not_published
340 +Rivière-Beaudette,71005,Montérégie,unifamilial,3.0,260,97.0,not_published
341 +Saint-Ambroise-de-Kildare,61040,Lanaudière,unifamilial,3.0,260,95.5,not_published
342 +Baie-D'Urfé,66112,Montréal,unifamilial,4.0,259,95.5,not_published
343 +Saint-Maurice,37230,Mauricie,unifamilial,4.0,256,94.0,not_published
344 +Mont-Royal,66072,Montréal,condo,3.0,256,97.0,not_published
345 +La Tuque,90012,Mauricie,plex,4.0,255,98.5,not_published
346 +Hampstead,66062,Montréal,unifamilial,3.0,254,97.0,not_published
347 +Rivière-du-Loup,12072,Bas-Saint-Laurent,plex,4.0,254,92.5,not_published
348 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,condo,3.0,252,89.6,not_published
349 +Mont-Laurier,79088,Laurentides,plex,3.0,251,94.0,not_published
350 +Saint-Isidore,26063,Chaudière-Appalaches,unifamilial,3.0,250,95.5,not_published
351 +Sept-Îles,97007,Côte-Nord,plex,3.0,247,94.0,not_published
352 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,unifamilial,4.0,247,89.6,not_published
353 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,unifamilial,3.0,246,92.5,not_published
354 +Crabtree,61013,Lanaudière,unifamilial,4.0,246,95.5,not_published
355 +Saint-Alphonse-de-Granby,47010,Estrie,unifamilial,3.0,245,100.0,not_published
356 +Berthierville,52035,Lanaudière,unifamilial,3.0,244,95.5,not_published
357 +Ormstown,69037,Montérégie,unifamilial,4.0,244,94.0,not_published
358 +Saint-Élie-de-Caxton,51075,Mauricie,unifamilial,3.0,244,97.0,not_published
359 +Saint-Gabriel-de-Valcartier,22025,Capitale-Nationale,unifamilial,3.0,244,92.5,not_published
360 +Saint-Basile,34038,Capitale-Nationale,unifamilial,4.0,243,94.0,not_published
361 +Sainte-Luce,09092,Bas-Saint-Laurent,unifamilial,4.0,243,94.0,not_published
362 +Malartic,89015,Abitibi-Témiscamingue,unifamilial,4.0,242,94.0,not_published
363 +Saint-Gabriel,52080,Lanaudière,unifamilial,4.0,241,91.0,not_published
364 +L'Islet,17078,Chaudière-Appalaches,unifamilial,3.0,241,94.0,not_published
365 +Sainte-Catherine-de-Hatley,45060,Estrie,unifamilial,3.5,240,92.5,not_published
366 +Blainville,73015,Laurentides,condo,3.0,239,86.6,not_published
367 +Sainte-Croix,33102,Chaudière-Appalaches,unifamilial,3.0,239,91.0,not_published
368 +Notre-Dame-des-Bois,30010,Estrie,unifamilial,3.0,238,97.0,not_published
369 +Lacolle,56023,Montérégie,unifamilial,3.0,236,97.0,not_published
370 +Sainte-Lucie-des-Laurentides,78020,Laurentides,unifamilial,4.0,235,95.5,not_published
371 +Cowansville,46080,Estrie,plex,4.0,234,94.0,not_published
372 +Papineauville,80037,Outaouais,unifamilial,4.0,234,92.5,not_published
373 +Beauharnois,70022,Montérégie,plex,3.0,231,98.5,not_published
374 +Saint-Denis-sur-Richelieu,57068,Montérégie,unifamilial,3.0,230,91.0,not_published
375 +Westmount,66032,Montréal,condo,3.0,230,95.5,not_published
376 +Saint-Liboire,54072,Montérégie,unifamilial,3.0,230,94.0,not_published
377 +Léry,67055,Montérégie,unifamilial,3.0,230,92.5,not_published
378 +Mont-Tremblant,78102,Laurentides,plex,3.0,229,92.5,not_published
379 +Vaudreuil-Dorion,71083,Montérégie,condo,3.0,229,76.1,not_published
380 +Saint-Christophe-d'Arthabaska,39060,Centre-du-Québec,unifamilial,3.0,228,94.0,not_published
381 +Ascot Corner,41055,Estrie,unifamilial,3.0,228,91.0,not_published
382 +New Richmond,05070,Gaspésie–Îles-de-la-Madeleine,unifamilial,3.0,226,98.5,not_published
383 +L'Assomption,60028,Lanaudière,plex,3.0,226,95.5,not_published
384 +Mont-Saint-Grégoire,56097,Montérégie,unifamilial,3.0,224,95.5,not_published
385 +Montpellier,80090,Outaouais,unifamilial,3.0,223,91.0,not_published
386 +Saint-Zénon,62080,Lanaudière,unifamilial,3.0,223,86.6,not_published
387 +Trois-Pistoles,11040,Bas-Saint-Laurent,unifamilial,3.0,222,94.0,not_published
388 +Saint-Raphaël,19082,Chaudière-Appalaches,unifamilial,3.0,222,94.0,not_published
389 +Saint-Jean-Port-Joli,17070,Chaudière-Appalaches,unifamilial,3.0,222,97.0,not_published
390 +Saint-Mathieu-de-Beloeil,57045,Montérégie,unifamilial,3.0,221,89.6,not_published
391 +Sainte-Marie-Madeleine,54030,Montérégie,unifamilial,3.0,220,86.6,not_published
392 +Sainte-Clotilde,68020,Montérégie,unifamilial,3.0,220,91.0,not_published
393 +Ferme-Neuve,79097,Laurentides,unifamilial,3.0,219,91.0,not_published
394 +Sainte-Anne-de-Sabrevois,56060,Montérégie,unifamilial,3.0,218,91.0,not_published
395 +Saint-Colomban,75005,Laurentides,plex,3.0,217,91.0,not_published
396 +Sainte-Anne-de-Bellevue,66117,Montréal,unifamilial,3.0,216,88.1,not_published
397 +Saint-Alexandre,56055,Montérégie,unifamilial,3.0,216,97.0,not_published
398 +Sainte-Thècle,35050,Mauricie,unifamilial,4.0,215,91.0,not_published
399 +Daveluyville,39152,Centre-du-Québec,unifamilial,3.0,215,92.5,not_published
400 +La Conception,78115,Laurentides,unifamilial,3.0,214,97.0,not_published
401 +Saint-Mathieu-du-Parc,51070,Mauricie,unifamilial,3.0,214,97.0,not_published
402 +Saint-Amable,59015,Montérégie,condo,3.0,213,79.1,not_published
403 +Saint-Louis-de-Gonzague,70035,Montérégie,unifamilial,3.0,212,94.0,not_published
404 +Sainte-Marcelline-de-Kildare,62030,Lanaudière,unifamilial,3.0,211,94.0,not_published
405 +Témiscaming,85005,Abitibi-Témiscamingue,unifamilial,3.0,211,94.0,not_published
406 +Notre-Dame-de-Lourdes,61045,Lanaudière,unifamilial,3.0,211,91.0,not_published
407 +Saint-Hippolyte,75045,Laurentides,plex,3.0,210,91.0,not_published
408 +Hinchinbrooke,69045,Montérégie,unifamilial,3.0,209,92.5,not_published
409 +Saint-Dominique,54060,Montérégie,unifamilial,3.0,208,89.6,not_published
410 +Brigham,46090,Estrie,unifamilial,3.0,208,89.6,not_published
411 +Saint-Marc-des-Carrières,34065,Capitale-Nationale,unifamilial,2.0,208,95.5,not_published
412 +Rougemont,55037,Montérégie,unifamilial,3.0,207,92.5,not_published
413 +Amos,88057,Abitibi-Témiscamingue,unifamilial,4.0,206,47.8,not_published
414 +Saint-Barthélemy,52055,Lanaudière,unifamilial,3.0,205,94.0,not_published
415 +Montréal-Ouest,66047,Montréal,unifamilial,3.0,205,89.6,not_published
416 +Farnham,46112,Estrie,plex,2.0,205,91.0,not_published
417 +Clermont,15035,Capitale-Nationale,unifamilial,3.0,205,86.6,not_published
418 +Entrelacs,62053,Lanaudière,unifamilial,3.0,204,94.0,not_published
419 +Saint-Stanislas-de-Kostka,70040,Montérégie,unifamilial,3.0,203,94.0,not_published
420 +Disraeli,31015,Chaudière-Appalaches,unifamilial,3.0,203,92.5,not_published
421 +Saint-Polycarpe,71020,Montérégie,unifamilial,3.0,203,91.0,not_published
422 +Stanstead,45008,Estrie,unifamilial,3.0,202,95.5,not_published
423 +Val-des-Bois,80140,Outaouais,unifamilial,3.0,202,79.1,not_published
424 +Saint-Côme–Linière,29057,Chaudière-Appalaches,unifamilial,3.0,201,89.6,not_published
425 +Scott,26048,Chaudière-Appalaches,unifamilial,3.0,200,94.0,not_published
426 +Saint-Lucien,49030,Centre-du-Québec,unifamilial,3.0,199,91.0,not_published
427 +Ange-Gardien,55008,Montérégie,unifamilial,3.0,199,95.5,not_published
428 +Mille-Isles,76030,Laurentides,unifamilial,3.0,198,97.0,not_published
429 +Lac-aux-Sables,35010,Mauricie,unifamilial,3.0,198,86.6,not_published
430 +Saint-Michel,68050,Montérégie,unifamilial,3.0,197,89.6,not_published
431 +Cap-Saint-Ignace,18045,Chaudière-Appalaches,unifamilial,3.0,197,92.5,not_published
432 +Saint-Chrysostome,69017,Montérégie,unifamilial,3.0,197,92.5,not_published
433 +Normandin,92040,Saguenay–Lac-Saint-Jean,unifamilial,2.0,196,95.5,not_published
434 +Saint-Charles-de-Bellechasse,19097,Chaudière-Appalaches,unifamilial,3.0,196,88.1,not_published
435 +Saint-Joseph-de-Coleraine,31045,Chaudière-Appalaches,unifamilial,3.0,196,92.5,not_published
436 +Stoke,42005,Estrie,unifamilial,3.0,195,94.0,not_published
437 +Harrington,76065,Laurentides,unifamilial,3.0,195,88.1,not_published
438 +Saint-Blaise-sur-Richelieu,56065,Montérégie,unifamilial,3.0,195,94.0,not_published
439 +Chambly,57005,Montérégie,plex,3.0,195,91.0,not_published
440 +L'Ascension-de-Notre-Seigneur,93065,Saguenay–Lac-Saint-Jean,unifamilial,3.0,195,83.6,not_published
441 +Dolbeau-Mistassini,92022,Saguenay–Lac-Saint-Jean,plex,3.0,195,85.1,not_published
442 +Berthier-sur-Mer,18065,Chaudière-Appalaches,unifamilial,2.0,195,91.0,not_published
443 +Ripon,80078,Outaouais,unifamilial,3.0,194,91.0,not_published
444 +Stratford,30110,Estrie,unifamilial,3.0,193,95.5,not_published
445 +Sainte-Anne-de-Sorel,53065,Montérégie,unifamilial,3.0,192,92.5,not_published
446 +Saint-François-du-Lac,50128,Centre-du-Québec,unifamilial,3.0,192,89.6,not_published
447 +Sainte-Barbe,69065,Montérégie,unifamilial,3.0,192,88.1,not_published
448 +Charlemagne,60005,Lanaudière,unifamilial,3.0,191,92.5,not_published
449 +Saint-Thomas,61027,Lanaudière,unifamilial,3.0,190,88.1,not_published
450 +Saint-Elzéar,26022,Chaudière-Appalaches,unifamilial,3.0,188,88.1,not_published
451 +Saint-Sulpice,60020,Lanaudière,unifamilial,3.0,188,83.6,not_published
452 +Pohénégamook,13095,Bas-Saint-Laurent,unifamilial,3.0,188,91.0,not_published
453 +Deschambault-Grondines,34058,Capitale-Nationale,unifamilial,2.5,188,89.6,not_published
454 +Richmond,42098,Estrie,unifamilial,3.0,188,82.1,not_published
455 +Bolton-Est,45095,Estrie,unifamilial,2.5,187,95.5,not_published
456 +Matane,08053,Bas-Saint-Laurent,plex,2.5,187,92.5,not_published
457 +Lebel-sur-Quévillon,99005,Nord-du-Québec,unifamilial,3.0,186,86.6,not_published
458 +Macamic,87058,Abitibi-Témiscamingue,unifamilial,3.0,186,86.6,not_published
459 +Bedford,46035,Estrie,unifamilial,3.0,186,88.1,not_published
460 +Havre-Saint-Pierre,98040,Côte-Nord,unifamilial,3.0,184,95.5,not_published
461 +Waterville,44080,Estrie,unifamilial,3.0,184,88.1,not_published
462 +Brossard,58007,Montérégie,plex,3.0,184,83.6,not_published
463 +Boischatel,21045,Capitale-Nationale,condo,2.0,182,82.1,not_published
464 +Pierreville,50113,Centre-du-Québec,unifamilial,3.0,182,86.6,not_published
465 +Saint-Anaclet-de-Lessard,10030,Bas-Saint-Laurent,unifamilial,3.0,182,88.1,not_published
466 +Saint-Tite-des-Caps,21005,Capitale-Nationale,unifamilial,2.5,181,89.6,not_published
467 +Saint-Prime,91035,Saguenay–Lac-Saint-Jean,unifamilial,3.0,181,92.5,not_published
468 +Saint-Paul-d'Abbotsford,55015,Montérégie,unifamilial,3.0,180,89.6,not_published
469 +Senneterre,89040,Abitibi-Témiscamingue,unifamilial,3.0,179,88.1,not_published
470 +Saint-Bernard,26055,Chaudière-Appalaches,unifamilial,3.0,178,80.6,not_published
471 +Saint-Jean-Baptiste,57033,Montérégie,unifamilial,3.0,177,88.1,not_published
472 +Saint-Antoine-de-Tilly,33095,Chaudière-Appalaches,unifamilial,3.0,176,83.6,not_published
473 +Maskinongé,51008,Mauricie,unifamilial,3.0,176,89.6,not_published
474 +Larouche,94265,Saguenay–Lac-Saint-Jean,unifamilial,3.0,176,79.1,not_published
475 +Compton,44071,Estrie,unifamilial,3.0,176,88.1,not_published
476 +Percé,02005,Gaspésie–Îles-de-la-Madeleine,unifamilial,3.0,176,89.6,not_published
477 +Saint-Pascal,14018,Bas-Saint-Laurent,unifamilial,2.0,175,94.0,not_published
478 +Sainte-Victoire-de-Sorel,53025,Montérégie,unifamilial,2.0,175,89.6,not_published
479 +Beloeil,57040,Montérégie,plex,2.0,175,88.1,not_published
480 +Chute-Saint-Philippe,79065,Laurentides,unifamilial,3.0,173,88.1,not_published
481 +Mansfield-et-Pontefract,84065,Outaouais,unifamilial,3.0,173,83.6,not_published
482 +Saint-Isidore,67040,Montérégie,unifamilial,3.0,173,83.6,not_published
483 +Noyan,56015,Montérégie,unifamilial,3.0,172,82.1,not_published
484 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,unifamilial,2.0,172,92.5,not_published
485 +Messines,83060,Outaouais,unifamilial,3.0,172,88.1,not_published
486 +Wickham,49040,Centre-du-Québec,unifamilial,2.0,172,85.1,not_published
487 +Saint-Gédéon-de-Beauce,29013,Chaudière-Appalaches,unifamilial,3.0,171,89.6,not_published
488 +Laurier-Station,33060,Chaudière-Appalaches,unifamilial,2.0,171,91.0,not_published
489 +Maria,06005,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,170,91.0,not_published
490 +L'Isle-aux-Allumettes,84082,Outaouais,unifamilial,3.0,169,89.6,not_published
491 +Rimouski,10043,Bas-Saint-Laurent,condo,3.0,169,86.6,not_published
492 +Ville-Marie,85025,Abitibi-Témiscamingue,unifamilial,3.0,168,83.6,not_published
493 +Yamachiche,51020,Mauricie,unifamilial,3.0,168,86.6,not_published
494 +Saint-Antoine-sur-Richelieu,57075,Montérégie,unifamilial,3.0,168,79.1,not_published
495 +East Broughton,31122,Chaudière-Appalaches,unifamilial,2.0,168,88.1,not_published
496 +Piedmont,77030,Laurentides,condo,3.0,167,77.6,not_published
497 +Upton,48038,Montérégie,unifamilial,3.0,167,86.6,not_published
498 +Saint-Liguori,63065,Lanaudière,unifamilial,2.0,167,89.6,not_published
499 +Cap-Chat,04047,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,167,88.1,not_published
500 +Clarenceville,56010,Montérégie,unifamilial,2.0,167,86.6,not_published
501 +Val-des-Sources,40043,Estrie,plex,3.0,167,88.1,not_published
502 +Saint-Damien-de-Buckland,19030,Chaudière-Appalaches,unifamilial,2.0,167,94.0,not_published
503 +Hemmingford,68015,Montérégie,unifamilial,3.0,167,85.1,not_published
504 +Dégelis,13005,Bas-Saint-Laurent,unifamilial,3.0,166,85.1,not_published
505 +Duhamel,80135,Outaouais,unifamilial,2.0,166,85.1,not_published
506 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,unifamilial,3.0,166,79.1,not_published
507 +Paspébiac,05032,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,165,91.0,not_published
508 +Fermont,97035,Côte-Nord,unifamilial,7.5,164,29.9,not_published
509 +Lac-Bouchette,91005,Saguenay–Lac-Saint-Jean,unifamilial,3.0,164,85.1,not_published
510 +Saint-Malachie,19025,Chaudière-Appalaches,unifamilial,2.0,164,86.6,not_published
511 +Saint-François-Xavier-de-Brompton,42020,Estrie,unifamilial,2.0,163,94.0,not_published
512 +Lambton,30095,Estrie,unifamilial,3.0,162,89.6,not_published
513 +Saint-Damase,54017,Montérégie,unifamilial,2.0,162,92.5,not_published
514 +Sainte-Geneviève-de-Berthier,52040,Lanaudière,unifamilial,2.0,162,86.6,not_published
515 +Champlain,37220,Mauricie,unifamilial,3.0,162,79.1,not_published
516 +Saint-Ferdinand,32013,Centre-du-Québec,unifamilial,3.0,161,82.1,not_published
517 +Dudswell,41117,Estrie,unifamilial,2.0,161,91.0,not_published
518 +Saint-Fabien,10070,Bas-Saint-Laurent,unifamilial,2.5,161,89.6,not_published
519 +Sainte-Cécile-de-Milton,47055,Estrie,unifamilial,2.0,160,83.6,not_published
520 +Saint-Ours,53032,Montérégie,unifamilial,2.0,160,92.5,not_published
521 +Saint-Ubalde,34090,Capitale-Nationale,unifamilial,2.0,159,86.6,not_published
522 +La Macaza,79047,Laurentides,unifamilial,2.0,159,85.1,not_published
523 +Saint-Marc-sur-Richelieu,57050,Montérégie,unifamilial,3.0,158,83.6,not_published
524 +Barraute,88022,Abitibi-Témiscamingue,unifamilial,3.0,158,83.6,not_published
525 +Mont-Saint-Hilaire,57035,Montérégie,condo,3.0,157,58.2,not_published
526 +Labrecque,93055,Saguenay–Lac-Saint-Jean,unifamilial,3.0,157,85.1,not_published
527 +Saint-Placide,72043,Laurentides,unifamilial,3.0,157,79.1,not_published
528 +Val-des-Monts,82015,Outaouais,plex,2.0,156,88.1,not_published
529 +La Prairie,67015,Montérégie,condo,3.0,156,59.7,not_published
530 +Varennes,59020,Montérégie,condo,3.0,156,71.6,not_published
531 +Forestville,95045,Côte-Nord,unifamilial,2.0,155,83.6,not_published
532 +Déléage,83070,Outaouais,unifamilial,2.0,155,85.1,not_published
533 +Saint-Cuthbert,52062,Lanaudière,unifamilial,2.0,155,89.6,not_published
534 +Sainte-Marthe-sur-le-Lac,72015,Laurentides,plex,2.0,154,86.6,not_published
535 +Saint-Ulric,08073,Bas-Saint-Laurent,unifamilial,3.0,154,82.1,not_published
536 +Bonaventure,05045,Gaspésie–Îles-de-la-Madeleine,unifamilial,3.0,153,79.1,not_published
537 +Sainte-Clotilde-de-Horton,39117,Centre-du-Québec,unifamilial,2.0,152,86.6,not_published
538 +L'Ancienne-Lorette,23057,Capitale-Nationale,plex,3.0,152,74.6,not_published
539 +Prévost,75040,Laurentides,plex,2.0,152,83.6,not_published
540 +Boisbriand,73005,Laurentides,plex,2.0,152,85.1,not_published
541 +Saint-Nazaire,93045,Saguenay–Lac-Saint-Jean,unifamilial,3.0,151,80.6,not_published
542 +Marieville,55048,Montérégie,plex,2.0,151,85.1,not_published
543 +Notre-Dame-du-Bon-Conseil,49075,Centre-du-Québec,unifamilial,2.0,151,88.1,not_published
544 +Saint-Hubert-de-Rivière-du-Loup,12010,Bas-Saint-Laurent,unifamilial,2.0,150,85.1,not_published
545 +Saint-Martin,29045,Chaudière-Appalaches,unifamilial,2.0,150,80.6,not_published
546 +Saint-Amable,59015,Montérégie,plex,2.0,150,88.1,not_published
547 +Saint-Constant,67035,Montérégie,plex,2.5,148,77.6,not_published
548 +Val-des-Lacs,78100,Laurentides,unifamilial,2.0,148,82.1,not_published
549 +Saint-Fulgence,94235,Saguenay–Lac-Saint-Jean,unifamilial,2.0,147,83.6,not_published
550 +Saint-Mathieu,67005,Montérégie,unifamilial,2.0,147,88.1,not_published
551 +Armagh,19037,Chaudière-Appalaches,unifamilial,2.0,146,88.1,not_published
552 +Deux-Montagnes,72010,Laurentides,plex,2.0,146,80.6,not_published
553 +Chambord,91020,Saguenay–Lac-Saint-Jean,unifamilial,2.0,146,82.1,not_published
554 +Mirabel,74005,Laurentides,condo,2.0,145,71.6,not_published
555 +Montmagny,18050,Chaudière-Appalaches,plex,2.0,145,82.1,not_published
556 +Orford,45115,Estrie,condo,3.0,145,79.1,not_published
557 +Lavaltrie,52007,Lanaudière,plex,2.0,144,86.6,not_published
558 +Lac-Sainte-Marie,83020,Outaouais,unifamilial,2.0,144,80.6,not_published
559 +Boisbriand,73005,Laurentides,condo,2.0,144,79.1,not_published
560 +Rawdon,62037,Lanaudière,plex,2.0,144,83.6,not_published
561 +Lantier,78015,Laurentides,unifamilial,2.0,144,85.1,not_published
562 +Cayamant,83040,Outaouais,unifamilial,2.0,144,83.6,not_published
563 +La Prairie,67015,Montérégie,plex,2.0,144,83.6,not_published
564 +Coaticook,44037,Estrie,plex,2.0,143,92.5,not_published
565 +Kingsey Falls,39097,Centre-du-Québec,unifamilial,2.0,143,83.6,not_published
566 +Bury,41070,Estrie,unifamilial,2.0,142,88.1,not_published
567 +Yamaska,53072,Montérégie,unifamilial,2.0,142,86.6,not_published
568 +Val-David,78010,Laurentides,plex,2.0,142,88.1,not_published
569 +Saint-Victor,27008,Chaudière-Appalaches,unifamilial,2.0,142,85.1,not_published
570 +Saint-Ignace-de-Loyola,52045,Lanaudière,unifamilial,2.0,142,83.6,not_published
571 +Grande-Rivière,02015,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,141,86.6,not_published
572 +Hatley,45055,Estrie,unifamilial,2.0,141,80.6,not_published
573 +Roberval,91025,Saguenay–Lac-Saint-Jean,plex,2.0,141,77.6,not_published
574 +Les Escoumins,95025,Côte-Nord,unifamilial,2.0,140,82.1,not_published
575 +Shawville,84010,Outaouais,unifamilial,2.0,139,85.1,not_published
576 +Saint-Charles-sur-Richelieu,57057,Montérégie,unifamilial,2.0,139,85.1,not_published
577 +Chapais,99020,Nord-du-Québec,unifamilial,2.0,139,88.1,not_published
578 +Vaudreuil-Dorion,71083,Montérégie,plex,2.0,139,79.1,not_published
579 +Albanel,92030,Saguenay–Lac-Saint-Jean,unifamilial,2.0,138,85.1,not_published
580 +Saint-Félix-de-Kingsey,49005,Centre-du-Québec,unifamilial,2.0,138,85.1,not_published
581 +Saint-Charles-Borromée,61035,Lanaudière,plex,2.0,137,83.6,not_published
582 +Causapscal,07018,Bas-Saint-Laurent,unifamilial,2.0,137,80.6,not_published
583 +Lac-des-Plages,80130,Outaouais,unifamilial,2.0,136,85.1,not_published
584 +Lac-Mégantic,30030,Estrie,plex,2.0,136,80.6,not_published
585 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,unifamilial,2.0,136,74.6,not_published
586 +Sainte-Anne-des-Plaines,73035,Laurentides,plex,2.0,136,80.6,not_published
587 +Beaulac-Garthby,31008,Chaudière-Appalaches,unifamilial,2.0,136,77.6,not_published
588 +Sainte-Adèle,77022,Laurentides,condo,2.0,136,82.1,not_published
589 +L'Île-Perrot,71060,Montérégie,plex,2.0,136,82.1,not_published
590 +Saint-Léonard-de-Portneuf,34115,Capitale-Nationale,unifamilial,2.0,136,89.6,not_published
591 +Châteauguay,67050,Montérégie,condo,2.5,135,65.7,not_published
592 +Henryville,56042,Montérégie,unifamilial,2.0,135,82.1,not_published
593 +Rivière-Héva,89010,Abitibi-Témiscamingue,unifamilial,2.0,135,82.1,not_published
594 +Bois-des-Filion,73030,Laurentides,condo,2.0,134,71.6,not_published
595 +Beaupré,21025,Capitale-Nationale,condo,2.0,134,77.6,not_published
596 +Pincourt,71070,Montérégie,condo,2.0,134,70.1,not_published
597 +Stanstead,45025,Estrie,unifamilial,2.0,134,83.6,not_published
598 +Sainte-Ursule,51040,Mauricie,unifamilial,2.0,134,83.6,not_published
599 +La Présentation,54035,Montérégie,unifamilial,2.0,134,82.1,not_published
600 +Saint-Michel-de-Bellechasse,19110,Chaudière-Appalaches,unifamilial,2.0,133,82.1,not_published
601 +Saint-Pamphile,17010,Chaudière-Appalaches,unifamilial,2.0,132,88.1,not_published
602 +Les Éboulements,16048,Capitale-Nationale,unifamilial,2.0,132,85.1,not_published
603 +Sainte-Anne-de-la-Pérade,37205,Mauricie,unifamilial,2.0,132,80.6,not_published
604 +Otter Lake,84055,Outaouais,unifamilial,2.0,131,82.1,not_published
605 +Saint-Alban,34097,Capitale-Nationale,unifamilial,2.0,131,73.1,not_published
606 +La Malbaie,15013,Capitale-Nationale,plex,2.0,129,82.1,not_published
607 +Pointe-des-Cascades,71055,Montérégie,unifamilial,2.0,129,82.1,not_published
608 +Caplan,05060,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,129,86.6,not_published
609 +Sainte-Christine-d'Auvergne,34105,Capitale-Nationale,unifamilial,2.0,129,85.1,not_published
610 +Bowman,80145,Outaouais,unifamilial,2.0,128,83.6,not_published
611 +Louiseville,51015,Mauricie,plex,2.0,128,82.1,not_published
612 +Sainte-Hélène-de-Bagot,54095,Montérégie,unifamilial,2.0,127,74.6,not_published
613 +Sainte-Catherine,67030,Montérégie,plex,2.0,126,80.6,not_published
614 +Valcourt,42055,Estrie,unifamilial,3.0,126,70.1,not_published
615 +Saint-Jacques-le-Mineur,68040,Montérégie,unifamilial,2.0,126,76.1,not_published
616 +Tingwick,39025,Centre-du-Québec,unifamilial,2.0,126,79.1,not_published
617 +Grand-Remous,83095,Outaouais,unifamilial,2.0,126,79.1,not_published
618 +Sayabec,07085,Bas-Saint-Laurent,unifamilial,2.0,126,86.6,not_published
619 +Saint-Bernard-de-Lacolle,68005,Montérégie,unifamilial,2.0,126,73.1,not_published
620 +Charlemagne,60005,Lanaudière,condo,2.0,126,67.2,not_published
621 +Montcalm,78055,Laurentides,unifamilial,3.0,125,76.1,not_published
622 +Hérouxville,35035,Mauricie,unifamilial,2.0,125,85.1,not_published
623 +Trois-Rives,35055,Mauricie,unifamilial,2.0,125,79.1,not_published
624 +L'Épiphanie,60037,Lanaudière,plex,2.0,125,85.1,not_published
625 +Grenville,76055,Laurentides,unifamilial,2.0,125,86.6,not_published
626 +Saint-Alexandre-de-Kamouraska,14035,Bas-Saint-Laurent,unifamilial,2.0,124,74.6,not_published
627 +Bristol,84005,Outaouais,unifamilial,2.0,124,76.1,not_published
628 +Sainte-Madeleine,54025,Montérégie,unifamilial,2.0,124,76.1,not_published
629 +Windsor,42088,Estrie,plex,2.0,124,82.1,not_published
630 +Frontenac,30025,Estrie,unifamilial,2.0,124,83.6,not_published
631 +Sainte-Justine,28045,Chaudière-Appalaches,unifamilial,2.0,124,76.1,not_published
632 +Low,83010,Outaouais,unifamilial,2.0,124,85.1,not_published
633 +Saint-Robert,53020,Montérégie,unifamilial,2.0,123,85.1,not_published
634 +Saint-Albert,39085,Centre-du-Québec,unifamilial,2.0,122,83.6,not_published
635 +Price,09065,Bas-Saint-Laurent,unifamilial,2.0,122,79.1,not_published
636 +Saint-Herménégilde,44015,Estrie,unifamilial,2.0,122,76.1,not_published
637 +Ayer's Cliff,45035,Estrie,unifamilial,2.0,121,80.6,not_published
638 +Donnacona,34025,Capitale-Nationale,plex,2.0,121,77.6,not_published
639 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,unifamilial,2.0,121,74.6,not_published
640 +Saint-Valérien-de-Milton,54065,Montérégie,unifamilial,2.0,120,83.6,not_published
641 +Wotton,40017,Estrie,unifamilial,2.0,120,82.1,not_published
642 +Cacouna,12057,Bas-Saint-Laurent,unifamilial,2.0,120,77.6,not_published
643 +Senneterre,89045,Abitibi-Témiscamingue,unifamilial,2.0,119,82.1,not_published
644 +Cleveland,42110,Estrie,unifamilial,2.0,119,77.6,not_published
645 +Lyster,32065,Centre-du-Québec,unifamilial,2.0,119,76.1,not_published
646 +Contrecoeur,59035,Montérégie,plex,2.0,119,74.6,not_published
647 +Saint-Félix-d'Otis,94225,Saguenay–Lac-Saint-Jean,unifamilial,2.0,119,71.6,not_published
648 +Saint-Gervais,19075,Chaudière-Appalaches,unifamilial,2.0,119,82.1,not_published
649 +Saint-Louis-de-Blandford,39170,Centre-du-Québec,unifamilial,2.0,118,74.6,not_published
650 +Saint-Aubert,17055,Chaudière-Appalaches,unifamilial,2.0,118,73.1,not_published
651 +Notre-Dame-de-Pontmain,79010,Laurentides,unifamilial,2.0,118,83.6,not_published
652 +Saint-Esprit,63030,Lanaudière,unifamilial,2.0,117,82.1,not_published
653 +Saint-Paulin,51060,Mauricie,unifamilial,2.0,117,80.6,not_published
654 +Saint-Zacharie,28005,Chaudière-Appalaches,unifamilial,2.0,117,74.6,not_published
655 +Vallée-Jonction,26015,Chaudière-Appalaches,unifamilial,2.0,117,82.1,not_published
656 +Frampton,26005,Chaudière-Appalaches,unifamilial,2.0,117,73.1,not_published
657 +Saint-Théodore-d'Acton,48045,Montérégie,unifamilial,2.0,117,77.6,not_published
658 +Bois-des-Filion,73030,Laurentides,plex,2.0,117,73.1,not_published
659 +Saint-Clet,71045,Montérégie,unifamilial,2.0,116,79.1,not_published
660 +Saint-Sauveur,77043,Laurentides,condo,2.0,116,73.1,not_published
661 +Pointe-aux-Outardes,96030,Côte-Nord,unifamilial,2.0,116,77.6,not_published
662 +Port-Daniel–Gascons,02047,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,116,80.6,not_published
663 +L'Isle-Verte,12043,Bas-Saint-Laurent,unifamilial,2.0,116,74.6,not_published
664 +Denholm,83005,Outaouais,unifamilial,2.0,115,76.1,not_published
665 +Saint-Félicien,91042,Saguenay–Lac-Saint-Jean,plex,2.0,115,83.6,not_published
666 +Stukely-Sud,45105,Estrie,unifamilial,2.0,115,76.1,not_published
667 +L'Île-Perrot,71060,Montérégie,condo,2.0,115,70.1,not_published
668 +Cantley,82020,Outaouais,plex,2.0,114,71.6,not_published
669 +Les Îles-de-la-Madeleine,01023,Gaspésie–Îles-de-la-Madeleine,plex,2.0,114,83.6,not_published
670 +Carignan,57010,Montérégie,condo,2.0,114,74.6,not_published
671 +Disraeli,31020,Chaudière-Appalaches,unifamilial,2.0,114,83.6,not_published
672 +Notre-Dame-de-Montauban,35005,Mauricie,unifamilial,2.0,113,85.1,not_published
673 +Chénéville,80103,Outaouais,unifamilial,2.0,113,77.6,not_published
674 +Saint-Alexis,63023,Lanaudière,unifamilial,2.0,113,76.1,not_published
675 +L'Ascension,79050,Laurentides,unifamilial,2.0,113,64.2,not_published
676 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,unifamilial,2.0,113,71.6,not_published
677 +Contrecoeur,59035,Montérégie,condo,2.0,113,70.1,not_published
678 +Saint-Joachim-de-Shefford,47040,Estrie,unifamilial,2.0,112,73.1,not_published
679 +Eeyou Istchee Baie-James,99060,Nord-du-Québec,unifamilial,2.0,112,83.6,not_published
680 +Lac-Sergent,34120,Capitale-Nationale,unifamilial,2.0,112,76.1,not_published
681 +Mont-Carmel,14005,Bas-Saint-Laurent,unifamilial,2.0,112,79.1,not_published
682 +Sainte-Angèle-de-Monnoir,55030,Montérégie,unifamilial,2.0,112,76.1,not_published
683 +Val-Joli,42095,Estrie,unifamilial,2.0,112,85.1,not_published
684 +McMasterville,57025,Montérégie,plex,2.0,111,80.6,not_published
685 +Montréal-Est,66007,Montréal,unifamilial,2.0,111,85.1,not_published
686 +Wentworth,76035,Laurentides,unifamilial,2.0,111,79.1,not_published
687 +La Doré,91050,Saguenay–Lac-Saint-Jean,unifamilial,2.0,110,79.1,not_published
688 +Rivière-à-Pierre,34135,Capitale-Nationale,unifamilial,2.0,110,71.6,not_published
689 +Saint-Gabriel-de-Rimouski,09025,Bas-Saint-Laurent,unifamilial,2.0,110,73.1,not_published
690 +Saint-Raymond,34128,Capitale-Nationale,plex,2.0,110,74.6,not_published
691 +Petite-Rivière-Saint-François,16005,Capitale-Nationale,unifamilial,2.0,110,82.1,not_published
692 +Saint-Aimé-des-Lacs,15030,Capitale-Nationale,unifamilial,2.0,110,83.6,not_published
693 +Sainte-Agathe-de-Lotbinière,33017,Chaudière-Appalaches,unifamilial,2.0,110,85.1,not_published
694 +Saint-Claude,42100,Estrie,unifamilial,2.0,110,71.6,not_published
695 +Notre-Dame-de-la-Salette,80087,Outaouais,unifamilial,2.0,110,76.1,not_published
696 +Saint-Jean-de-Dieu,11010,Bas-Saint-Laurent,unifamilial,2.0,110,79.1,not_published
697 +Saint-Narcisse-de-Rimouski,10015,Bas-Saint-Laurent,unifamilial,2.0,109,73.1,not_published
698 +Saint-Hugues,54100,Montérégie,unifamilial,2.0,109,77.6,not_published
699 +Saint-Siméon,15058,Capitale-Nationale,unifamilial,2.0,109,74.6,not_published
700 +Franklin,69010,Montérégie,unifamilial,2.0,109,86.6,not_published
701 +Saint-Didace,52090,Lanaudière,unifamilial,2.0,108,82.1,not_published
702 +Pointe-Lebel,96025,Côte-Nord,unifamilial,2.0,108,71.6,not_published
703 +Varennes,59020,Montérégie,plex,2.0,107,73.1,not_published
704 +Saint-Mathieu-de-Rioux,11050,Bas-Saint-Laurent,unifamilial,2.0,107,71.6,not_published
705 +Saint-Justin,51045,Mauricie,unifamilial,1.0,107,79.1,not_published
706 +L'Assomption,60028,Lanaudière,condo,2.0,106,67.2,not_published
707 +Lamarche,93060,Saguenay–Lac-Saint-Jean,unifamilial,2.0,106,76.1,not_published
708 +Saint-Éphrem-de-Beauce,29112,Chaudière-Appalaches,unifamilial,2.0,106,74.6,not_published
709 +Saint-Joachim,21020,Capitale-Nationale,unifamilial,2.0,106,74.6,not_published
710 +Notre-Dame-du-Portage,12080,Bas-Saint-Laurent,unifamilial,2.0,106,79.1,not_published
711 +Saint-Benoît-Labre,29100,Chaudière-Appalaches,unifamilial,2.0,106,74.6,not_published
712 +Sainte-Julie,59010,Montérégie,plex,2.0,105,79.1,not_published
713 +Saint-Eugène,49105,Centre-du-Québec,unifamilial,2.0,105,71.6,not_published
714 +Saint-Armand,46017,Estrie,unifamilial,2.0,105,71.6,not_published
715 +Sacré-Coeur,95010,Côte-Nord,unifamilial,2.0,105,73.1,not_published
716 +Nantes,30045,Estrie,unifamilial,2.0,105,71.6,not_published
717 +Saint-Casimir,34078,Capitale-Nationale,unifamilial,2.0,105,74.6,not_published
718 +Saint-Adelphe,35015,Mauricie,unifamilial,2.0,105,70.1,not_published
719 +Saint-Pierre-les-Becquets,38065,Centre-du-Québec,unifamilial,2.0,104,58.2,not_published
720 +Batiscan,37210,Mauricie,unifamilial,1.0,104,82.1,not_published
721 +Sainte-Hénédine,26040,Chaudière-Appalaches,unifamilial,2.0,104,71.6,not_published
722 +Saint-Cyprien-de-Napierville,68035,Montérégie,unifamilial,2.0,104,79.1,not_published
723 +Mont-Joli,09077,Bas-Saint-Laurent,plex,2.0,103,76.1,not_published
724 +Saint-Guillaume,49113,Centre-du-Québec,unifamilial,2.0,103,74.6,not_published
725 +Bouchette,83050,Outaouais,unifamilial,2.0,103,79.1,not_published
726 +Sainte-Geneviève-de-Batiscan,37215,Mauricie,unifamilial,2.0,103,71.6,not_published
727 +Saint-Narcisse,37240,Mauricie,unifamilial,1.0,103,74.6,not_published
728 +Montréal-Est,66007,Montréal,plex,2.0,103,76.1,not_published
729 +L'Ancienne-Lorette,23057,Capitale-Nationale,condo,1.0,103,79.1,not_published
730 +Racine,42032,Estrie,unifamilial,2.0,102,71.6,not_published
731 +Blue Sea,83045,Outaouais,unifamilial,2.0,101,73.1,not_published
732 +Sainte-Brigide-d'Iberville,56105,Montérégie,unifamilial,2.0,101,77.6,not_published
733 +Boucherville,58033,Montérégie,plex,2.0,101,76.1,not_published
734 +Preissac,88090,Abitibi-Témiscamingue,unifamilial,2.0,100,73.1,not_published
735 +Saint-Simon,54090,Montérégie,unifamilial,2.0,100,67.2,not_published
736 +Matagami,99015,Nord-du-Québec,unifamilial,2.0,100,68.7,not_published
737 +Tring-Jonction,27060,Chaudière-Appalaches,unifamilial,2.0,100,64.2,not_published
738 +Brownsburg-Chatham,76043,Laurentides,plex,2.0,100,71.6,not_published
739 +Saint-Pacôme,14070,Bas-Saint-Laurent,unifamilial,2.0,99,77.6,not_published
740 +Nicolet,50072,Centre-du-Québec,plex,2.0,99,76.1,not_published
741 +Notre-Dame-du-Nord,85090,Abitibi-Témiscamingue,unifamilial,2.0,99,77.6,not_published
742 +Aumond,83090,Outaouais,unifamilial,2.0,99,70.1,not_published
743 +Sainte-Jeanne-d'Arc,92015,Saguenay–Lac-Saint-Jean,unifamilial,2.0,99,68.7,not_published
744 +Kazabazua,83015,Outaouais,unifamilial,2.0,99,74.6,not_published
745 +Terrasse-Vaudreuil,71075,Montérégie,unifamilial,2.0,99,71.6,not_published
746 +Saint-Paul-de-Montminy,18030,Chaudière-Appalaches,unifamilial,2.0,98,68.7,not_published
747 +Rivière-Bleue,13025,Bas-Saint-Laurent,unifamilial,2.0,98,73.1,not_published
748 +Saint-Patrice-de-Sherrington,68025,Montérégie,unifamilial,1.0,98,77.6,not_published
749 +Nouvelle,06020,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,98,76.1,not_published
750 +Bromont,46078,Estrie,condo,1.0,98,73.1,not_published
751 +Saint-Joseph-de-Sorel,53050,Montérégie,unifamilial,2.0,97,70.1,not_published
752 +Saint-Jean-de-l'Île-d'Orléans,20015,Capitale-Nationale,unifamilial,2.0,97,61.2,not_published
753 +Roxton Falls,48010,Montérégie,unifamilial,2.0,97,71.6,not_published
754 +Sainte-Aurélie,28015,Chaudière-Appalaches,unifamilial,2.0,97,73.1,not_published
755 +Desbiens,93005,Saguenay–Lac-Saint-Jean,unifamilial,2.0,97,74.6,not_published
756 +L'Avenir,49025,Centre-du-Québec,unifamilial,2.0,97,73.1,not_published
757 +Clarendon,84015,Outaouais,unifamilial,2.0,97,71.6,not_published
758 +Montebello,80010,Outaouais,unifamilial,2.0,97,73.1,not_published
759 +La Sarre,87090,Abitibi-Témiscamingue,plex,2.0,96,74.6,not_published
760 +Frelighsburg,46010,Estrie,unifamilial,2.0,96,74.6,not_published
761 +Saint-Joseph-de-Sorel,53050,Montérégie,plex,2.0,96,71.6,not_published
762 +Kiamika,79025,Laurentides,unifamilial,2.0,96,68.7,not_published
763 +Sainte-Perpétue,17030,Chaudière-Appalaches,unifamilial,1.5,96,71.6,not_published
764 +Charlemagne,60005,Lanaudière,plex,2.0,96,74.6,not_published
765 +Thurso,80050,Outaouais,plex,2.0,96,62.7,not_published
766 +Saint-Georges-de-Windsor,40032,Estrie,unifamilial,2.0,95,73.1,not_published
767 +Notre-Dame-des-Neiges,11045,Bas-Saint-Laurent,unifamilial,2.0,95,62.7,not_published
768 +Berthierville,52035,Lanaudière,plex,2.0,94,73.1,not_published
769 +Saint-Benjamin,28025,Chaudière-Appalaches,unifamilial,1.0,94,77.6,not_published
770 +Saint-René-de-Matane,08035,Bas-Saint-Laurent,unifamilial,1.0,94,79.1,not_published
771 +Saint-Édouard-de-Lotbinière,33080,Chaudière-Appalaches,unifamilial,2.0,94,74.6,not_published
772 +Saint-Lambert,58012,Montérégie,plex,2.0,94,73.1,not_published
773 +Saint-Jude,54110,Montérégie,unifamilial,2.0,94,73.1,not_published
774 +Saint-Aimé-du-Lac-des-Îles,79022,Laurentides,unifamilial,2.0,93,71.6,not_published
775 +La Patrie,41027,Estrie,unifamilial,1.0,93,79.1,not_published
776 +Saint-Louis,54120,Montérégie,unifamilial,2.0,93,67.2,not_published
777 +Lorrainville,85037,Abitibi-Témiscamingue,unifamilial,2.0,93,74.6,not_published
778 +Marieville,55048,Montérégie,condo,2.0,93,56.7,not_published
779 +East Angus,41060,Estrie,plex,2.0,93,77.6,not_published
780 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,condo,1.0,93,70.1,not_published
781 +Palmarolle,87025,Abitibi-Témiscamingue,unifamilial,2.0,92,68.7,not_published
782 +Inverness,32058,Centre-du-Québec,unifamilial,1.0,92,71.6,not_published
783 +La Guadeloupe,29030,Chaudière-Appalaches,unifamilial,1.0,92,70.1,not_published
784 +Saint-Basile-le-Grand,57020,Montérégie,condo,2.0,92,50.7,not_published
785 +Sainte-Rose-de-Watford,28030,Chaudière-Appalaches,unifamilial,2.0,91,73.1,not_published
786 +Saint-Léon-de-Standon,19020,Chaudière-Appalaches,unifamilial,2.0,91,68.7,not_published
787 +Lac-Saguay,79060,Laurentides,unifamilial,2.0,91,71.6,not_published
788 +Notre-Dame-Auxiliatrice-de-Buckland,19010,Chaudière-Appalaches,unifamilial,1.0,91,74.6,not_published
789 +Waterloo,47025,Estrie,plex,1.0,90,67.2,not_published
790 +Sainte-Marguerite,26035,Chaudière-Appalaches,unifamilial,1.0,90,67.2,not_published
791 +Mont-Blanc,78047,Laurentides,plex,2.0,90,65.7,not_published
792 +Saint-Nérée-de-Bellechasse,19045,Chaudière-Appalaches,unifamilial,2.0,90,65.7,not_published
793 +Marston,30035,Estrie,unifamilial,2.0,89,71.6,not_published
794 +Drummondville,49058,Centre-du-Québec,condo,2.0,89,67.2,published
795 +Saint-Philémon,19005,Chaudière-Appalaches,unifamilial,2.0,89,71.6,not_published
796 +Saint-Cyrille-de-Lessard,17045,Chaudière-Appalaches,unifamilial,2.0,89,73.1,not_published
797 +Saint-Odilon-de-Cranbourne,27035,Chaudière-Appalaches,unifamilial,2.0,88,71.6,not_published
798 +Val-Alain,33070,Chaudière-Appalaches,unifamilial,2.0,88,62.7,not_published
799 +Sainte-Sophie-de-Lévrard,38040,Centre-du-Québec,unifamilial,2.0,88,65.7,not_published
800 +Saint-Bruno-de-Guigues,85045,Abitibi-Témiscamingue,unifamilial,1.0,88,70.1,not_published
801 +Lac-du-Cerf,79015,Laurentides,unifamilial,2.0,88,68.7,not_published
802 +Godmanchester,69060,Montérégie,unifamilial,2.0,88,73.1,not_published
803 +Très-Saint-Rédempteur,71125,Montérégie,unifamilial,2.0,87,65.7,not_published
804 +Richmond,42098,Estrie,plex,2.0,87,70.1,not_published
805 +La Corne,88030,Abitibi-Témiscamingue,unifamilial,2.0,87,58.2,not_published
806 +Brébeuf,78075,Laurentides,unifamilial,2.0,86,73.1,not_published
807 +Saint-Étienne-de-Bolton,45100,Estrie,unifamilial,1.0,86,74.6,not_published
808 +Saint-Urbain-Premier,70005,Montérégie,unifamilial,2.0,86,64.2,not_published
809 +Sainte-Flavie,09085,Bas-Saint-Laurent,unifamilial,2.0,86,62.7,not_published
810 +Saint-Ludger-de-Milot,93080,Saguenay–Lac-Saint-Jean,unifamilial,2.0,85,68.7,not_published
811 +Ham-Nord,39010,Centre-du-Québec,unifamilial,2.0,85,70.1,not_published
812 +Sorel-Tracy,53052,Montérégie,condo,2.0,85,68.7,not_published
813 +Saint-Valère,39135,Centre-du-Québec,unifamilial,2.0,85,70.1,not_published
814 +Saint-Fabien-de-Panet,18015,Chaudière-Appalaches,unifamilial,2.0,84,65.7,not_published
815 +Ragueneau,96040,Côte-Nord,unifamilial,2.0,84,68.7,not_published
816 +Saint-Rémi,68055,Montérégie,condo,2.0,84,50.7,not_published
817 +Saint-René,29050,Chaudière-Appalaches,unifamilial,1.0,84,76.1,not_published
818 +Baie-Saint-Paul,16013,Capitale-Nationale,plex,2.0,84,65.7,not_published
819 +Saint-Édouard-de-Maskinongé,51050,Mauricie,unifamilial,2.0,84,65.7,not_published
820 +North Hatley,45050,Estrie,unifamilial,2.0,84,65.7,not_published
821 +Deschaillons-sur-Saint-Laurent,38070,Centre-du-Québec,unifamilial,1.0,84,68.7,not_published
822 +Lac-Brome,46075,Estrie,condo,2.0,84,68.7,not_published
823 +Saint-Constant,67035,Montérégie,condo,2.0,83,43.3,not_published
824 +Grande-Vallée,03020,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,83,67.2,not_published
825 +Notre-Dame-des-Prairies,61030,Lanaudière,plex,2.0,83,62.7,not_published
826 +Saint-Norbert-d'Arthabaska,39043,Centre-du-Québec,unifamilial,1.0,83,76.1,not_published
827 +Fort-Coulonge,84060,Outaouais,unifamilial,2.0,83,61.2,not_published
828 +Saint-Magloire,28075,Chaudière-Appalaches,unifamilial,2.0,83,62.7,not_published
829 +Saint-Hilarion,16050,Capitale-Nationale,unifamilial,2.0,83,67.2,not_published
830 +Plaisance,80045,Outaouais,unifamilial,1.0,83,80.6,not_published
831 +Saint-Rémi,68055,Montérégie,plex,1.0,82,74.6,not_published
832 +Saint-Joseph-du-Lac,72025,Laurentides,plex,1.5,82,65.7,not_published
833 +Dosquet,33040,Chaudière-Appalaches,unifamilial,1.0,82,68.7,not_published
834 +Malartic,89015,Abitibi-Témiscamingue,plex,2.0,82,67.2,not_published
835 +Saint-Valérien,10060,Bas-Saint-Laurent,unifamilial,1.0,82,71.6,not_published
836 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,plex,1.0,82,70.1,not_published
837 +Sainte-Eulalie,50005,Centre-du-Québec,unifamilial,2.0,82,62.7,not_published
838 +Saint-Camille-de-Lellis,28070,Chaudière-Appalaches,unifamilial,1.0,82,70.1,not_published
839 +Saint-Donat,09030,Bas-Saint-Laurent,unifamilial,2.0,81,61.2,not_published
840 +Laurierville,32072,Centre-du-Québec,unifamilial,2.0,81,64.2,not_published
841 +Saint-Honoré-de-Shenley,29038,Chaudière-Appalaches,unifamilial,1.5,81,62.7,not_published
842 +Sainte-Monique,93075,Saguenay–Lac-Saint-Jean,unifamilial,1.0,81,62.7,not_published
843 +Lotbinière,33115,Chaudière-Appalaches,unifamilial,2.0,81,68.7,not_published
844 +Vaudreuil-sur-le-Lac,71090,Montérégie,unifamilial,1.0,81,73.1,not_published
845 +Girardville,92055,Saguenay–Lac-Saint-Jean,unifamilial,1.5,81,65.7,not_published
846 +Saint-Ludger,30072,Estrie,unifamilial,1.0,80,74.6,not_published
847 +Carignan,57010,Montérégie,plex,2.0,80,65.7,not_published
848 +Saint-Flavien,33052,Chaudière-Appalaches,unifamilial,2.0,80,64.2,not_published
849 +Saint-Janvier-de-Joly,33065,Chaudière-Appalaches,unifamilial,1.5,80,65.7,not_published
850 +Val-Brillant,07080,Bas-Saint-Laurent,unifamilial,2.0,79,58.2,not_published
851 +Saint-Majorique-de-Grantham,49095,Centre-du-Québec,unifamilial,1.0,79,73.1,not_published
852 +Saint-Barnabé,51025,Mauricie,unifamilial,1.0,79,67.2,not_published
853 +Gaspé,03005,Gaspésie–Îles-de-la-Madeleine,plex,2.0,79,62.7,not_published
854 +New Carlisle,05040,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,79,70.1,not_published
855 +Saint-Wenceslas,50023,Centre-du-Québec,unifamilial,1.0,79,68.7,not_published
856 +Saint-Siméon,05055,Gaspésie–Îles-de-la-Madeleine,unifamilial,2.0,79,64.2,not_published
857 +L'Isle-aux-Coudres,16023,Capitale-Nationale,unifamilial,2.0,79,68.7,not_published
858 +Lac-Delage,22030,Capitale-Nationale,unifamilial,2.0,79,52.2,not_published
859 +Saint-Césaire,55023,Montérégie,plex,1.0,79,61.2,not_published
860 +Manseau,38028,Centre-du-Québec,unifamilial,2.0,79,65.7,not_published
861 +Plessisville,32043,Centre-du-Québec,plex,1.0,78,59.7,not_published
862 +Saint-Donat,62060,Lanaudière,plex,1.0,78,65.7,not_published
863 +Bécancour,38010,Centre-du-Québec,plex,1.0,78,67.2,not_published
864 +Acton Vale,48028,Montérégie,plex,1.0,78,65.7,not_published
865 +Lac-Drolet,30080,Estrie,unifamilial,2.0,78,64.2,not_published
866 +Saint-Norbert,52070,Lanaudière,unifamilial,2.0,78,62.7,not_published
867 +Lac-au-Saumon,07057,Bas-Saint-Laurent,unifamilial,1.0,78,67.2,not_published
868 +Saint-François-de-la-Rivière-du-Sud,18060,Chaudière-Appalaches,unifamilial,1.0,77,65.7,not_published
869 +Saint-Augustin-de-Desmaures,23072,Capitale-Nationale,plex,2.0,77,61.2,not_published
870 +Saint-Luc-de-Bellechasse,28060,Chaudière-Appalaches,unifamilial,1.5,77,65.7,not_published
871 +Hemmingford,68010,Montérégie,unifamilial,1.0,77,68.7,not_published
872 +Saint-Épiphane,12030,Bas-Saint-Laurent,unifamilial,2.0,76,65.7,not_published
873 +Saint-Édouard,68045,Montérégie,unifamilial,1.0,76,64.2,not_published
874 +Chibougamau,99025,Nord-du-Québec,plex,1.5,76,62.7,not_published
875 +Laverlochère-Angliers,85052,Abitibi-Témiscamingue,unifamilial,1.5,76,59.7,not_published
876 +Crabtree,61013,Lanaudière,plex,1.0,76,67.2,not_published
877 +Saint-Félix-de-Valois,62007,Lanaudière,plex,1.5,76,65.7,not_published
878 +Rivière-Ouelle,14065,Bas-Saint-Laurent,unifamilial,1.0,76,67.2,not_published
879 +Boileau,80115,Outaouais,unifamilial,1.0,76,65.7,not_published
880 +Saint-Isidore-de-Clifton,41012,Estrie,unifamilial,1.0,76,59.7,not_published
881 +Chute-aux-Outardes,96035,Côte-Nord,unifamilial,1.5,76,65.7,not_published
882 +Saint-Henri-de-Taillon,93070,Saguenay–Lac-Saint-Jean,unifamilial,2.0,76,64.2,not_published
883 +Charette,51080,Mauricie,unifamilial,1.0,76,67.2,not_published
884 +Saint-Thomas-Didyme,92045,Saguenay–Lac-Saint-Jean,unifamilial,1.0,76,62.7,not_published
885 +Sainte-Marie-Salomé,63005,Lanaudière,unifamilial,2.0,75,64.2,not_published
886 +Sainte-Élisabeth,52030,Lanaudière,unifamilial,1.5,75,59.7,not_published
887 +Saint-Émile-de-Suffolk,80125,Outaouais,unifamilial,1.0,75,70.1,not_published
888 +Sutton,46058,Estrie,plex,2.0,75,64.2,not_published
889 +Chesterville,39030,Centre-du-Québec,unifamilial,2.0,75,52.2,not_published
890 +Saint-Georges,29073,Chaudière-Appalaches,condo,2.0,75,56.7,not_published
891 +Côte-Saint-Luc,66058,Montréal,plex,1.0,75,65.7,not_published
892 +Sainte-Pétronille,20030,Capitale-Nationale,unifamilial,1.0,75,64.2,not_published
893 +Sainte-Thérèse-de-la-Gatineau,83055,Outaouais,unifamilial,1.5,75,62.7,not_published
894 +Sainte-Anne-du-Lac,79115,Laurentides,unifamilial,1.0,74,62.7,not_published
895 +Richelieu,55057,Montérégie,plex,1.0,74,68.7,not_published
896 +Maniwaki,83065,Outaouais,plex,1.0,74,64.2,not_published
897 +Saint-Rémi-de-Tingwick,39020,Centre-du-Québec,unifamilial,1.0,73,62.7,not_published
898 +Saint-Séverin,35020,Mauricie,unifamilial,2.0,73,62.7,not_published
899 +Sainte-Anne-des-Monts,04037,Gaspésie–Îles-de-la-Madeleine,plex,1.0,73,61.2,not_published
900 +Notre-Dame-de-la-Paix,80020,Outaouais,unifamilial,1.0,73,65.7,not_published
901 +Hatley,45043,Estrie,unifamilial,2.0,73,58.2,not_published
902 +Bromont,46078,Estrie,plex,1.0,73,65.7,not_published
903 +Amqui,07047,Bas-Saint-Laurent,plex,2.0,73,56.7,not_published
904 +Namur,80110,Outaouais,unifamilial,1.0,73,64.2,not_published
905 +Dupuy,87085,Abitibi-Témiscamingue,unifamilial,2.0,72,59.7,not_published
906 +Saint-Louis-du-Ha! Ha!,13080,Bas-Saint-Laurent,unifamilial,1.5,72,59.7,not_published
907 +Lac-Saint-Paul,79105,Laurentides,unifamilial,1.0,72,58.2,not_published
908 +Trécesson,88075,Abitibi-Témiscamingue,unifamilial,2.0,72,65.7,not_published
909 +Saint-Irénée,15005,Capitale-Nationale,unifamilial,1.0,72,70.1,not_published
910 +Baie-du-Febvre,50100,Centre-du-Québec,unifamilial,1.0,72,62.7,not_published
911 +Estérel,77011,Laurentides,unifamilial,1.0,72,62.7,not_published
912 +Sainte-Sabine,46105,Estrie,unifamilial,1.0,71,59.7,not_published
913 +Saint-Marc-de-Figuery,88040,Abitibi-Témiscamingue,unifamilial,2.0,71,58.2,not_published
914 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,plex,2.0,71,61.2,not_published
915 +Saint-Roch-de-Mékinac,35045,Mauricie,unifamilial,2.0,71,56.7,not_published
916 +Durham-Sud,49015,Centre-du-Québec,unifamilial,1.0,71,58.2,not_published
917 +Saint-Modeste,12020,Bas-Saint-Laurent,unifamilial,2.0,71,55.2,not_published
918 +Hébertville,93022,Saguenay–Lac-Saint-Jean,plex,1.0,71,67.2,not_published
919 +Beauceville,27028,Chaudière-Appalaches,plex,1.0,71,67.2,not_published
920 +Grandes-Piles,35040,Mauricie,unifamilial,2.0,71,64.2,not_published
921 +Sainte-Hedwidge,91030,Saguenay–Lac-Saint-Jean,unifamilial,1.0,71,62.7,not_published
922 +Roxton,48015,Montérégie,unifamilial,1.0,71,65.7,not_published
923 +Cookshire-Eaton,41038,Estrie,plex,2.0,71,61.2,not_published
924 +Bolton-Ouest,46065,Estrie,unifamilial,1.0,71,68.7,not_published
925 +Landrienne,88035,Abitibi-Témiscamingue,unifamilial,1.0,70,65.7,not_published
926 +Très-Saint-Sacrement,69030,Montérégie,unifamilial,2.0,70,59.7,not_published
927 +Howick,69025,Montérégie,unifamilial,1.0,70,56.7,not_published
928 +Saint-Cyprien,12005,Bas-Saint-Laurent,unifamilial,1.0,70,55.2,not_published
929 +Morin-Heights,77050,Laurentides,plex,1.0,70,62.7,not_published
930 +Salaberry-de-Valleyfield,70052,Montérégie,condo,1.0,70,61.2,not_published
931 +Saint-Gabriel,52080,Lanaudière,plex,1.0,70,71.6,not_published
932 +Huberdeau,78065,Laurentides,unifamilial,1.0,70,59.7,not_published
933 +Montréal-Est,66007,Montréal,condo,1.0,70,61.2,not_published
934 +Kirkland,66102,Montréal,condo,1.0,69,55.2,not_published
935 +Bégin,94250,Saguenay–Lac-Saint-Jean,unifamilial,1.0,69,62.7,not_published
936 +Mulgrave-et-Derry,80085,Outaouais,unifamilial,1.0,69,64.2,not_published
937 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,unifamilial,1.0,69,65.7,not_published
938 +Sainte-Praxède,31050,Chaudière-Appalaches,unifamilial,1.0,69,64.2,not_published
939 +Sainte-Justine-de-Newton,71115,Montérégie,unifamilial,2.0,68,53.7,not_published
940 +Saint-Roch-des-Aulnaies,17065,Chaudière-Appalaches,unifamilial,2.0,68,59.7,not_published
941 +Mont-Saint-Michel,79110,Laurentides,unifamilial,1.0,68,62.7,not_published
942 +Saint-Étienne-de-Beauharnois,70030,Montérégie,unifamilial,1.0,68,65.7,not_published
943 +Joliette,61025,Lanaudière,condo,1.0,68,44.8,not_published
944 +Saint-Urbain,16055,Capitale-Nationale,unifamilial,1.0,68,61.2,not_published
945 +Valcourt,42060,Estrie,unifamilial,1.0,68,62.7,not_published
946 +Mayo,80065,Outaouais,unifamilial,1.0,68,56.7,not_published
947 +La Pêche,82035,Outaouais,plex,1.0,68,59.7,not_published
948 +Labelle,78120,Laurentides,plex,1.0,68,56.7,not_published
949 +Saint-Théophile,29005,Chaudière-Appalaches,unifamilial,1.0,68,65.7,not_published
950 +Béarn,85020,Abitibi-Témiscamingue,unifamilial,1.5,67,56.7,not_published
951 +Pont-Rouge,34017,Capitale-Nationale,plex,1.0,67,58.2,not_published
952 +Rigaud,71133,Montérégie,plex,2.0,67,53.7,not_published
953 +Sainte-Félicité,08023,Bas-Saint-Laurent,unifamilial,1.5,67,59.7,not_published
954 +Melbourne,42075,Estrie,unifamilial,1.0,66,56.7,not_published
955 +Thorne,84045,Outaouais,unifamilial,1.0,66,59.7,not_published
956 +Sainte-Cécile-de-Whitton,30050,Estrie,unifamilial,1.0,66,64.2,not_published
957 +La Durantaye,19090,Chaudière-Appalaches,unifamilial,2.0,66,52.2,not_published
958 +Saint-Zotique,71025,Montérégie,condo,1.0,66,61.2,not_published
959 +Sainte-Marie,26030,Chaudière-Appalaches,plex,1.0,66,58.2,not_published
960 +La Bostonnais,90017,Mauricie,unifamilial,1.0,66,58.2,not_published
961 +Saint-Pie,54008,Montérégie,plex,1.0,65,58.2,not_published
962 +Saint-Zotique,71025,Montérégie,plex,1.0,65,65.7,not_published
963 +Saints-Anges,26010,Chaudière-Appalaches,unifamilial,2.0,65,56.7,not_published
964 +Saint-Marcellin,10025,Bas-Saint-Laurent,unifamilial,1.0,65,59.7,not_published
965 +Litchfield,84040,Outaouais,unifamilial,1.0,65,58.2,not_published
966 +Saint-Sylvestre,33007,Chaudière-Appalaches,unifamilial,1.0,65,65.7,not_published
967 +Princeville,32033,Centre-du-Québec,plex,1.0,64,59.7,not_published
968 +Saint-Léon-le-Grand,07030,Bas-Saint-Laurent,unifamilial,1.0,64,55.2,not_published
969 +Bonsecours,42040,Estrie,unifamilial,1.0,64,55.2,not_published
970 +Saint-Tite,35027,Mauricie,plex,1.0,64,61.2,not_published
971 +Havelock,69005,Montérégie,unifamilial,1.0,64,61.2,not_published
972 +Mercier,67045,Montérégie,plex,1.0,64,61.2,not_published
973 +Piedmont,77030,Laurentides,plex,1.0,64,64.2,not_published
974 +Westmount,66032,Montréal,plex,1.0,64,61.2,not_published
975 +Saint-Jacques,63013,Lanaudière,plex,1.0,64,58.2,not_published
976 +Lefebvre,49020,Centre-du-Québec,unifamilial,1.0,64,53.7,not_published
977 +Saint-Pierre-Baptiste,32050,Centre-du-Québec,unifamilial,1.0,64,65.7,not_published
978 +Saint-Maxime-du-Mont-Louis,04010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,63,52.2,not_published
979 +Saint-Romain,30100,Estrie,unifamilial,1.0,63,59.7,not_published
980 +Saint-Narcisse-de-Beaurivage,33030,Chaudière-Appalaches,unifamilial,1.5,63,50.7,not_published
981 +Massueville,53010,Montérégie,unifamilial,1.0,63,59.7,not_published
982 +Trois-Pistoles,11040,Bas-Saint-Laurent,plex,2.0,63,58.2,not_published
983 +Saint-Calixte,63055,Lanaudière,plex,1.0,62,52.2,not_published
984 +Duparquet,87005,Abitibi-Témiscamingue,unifamilial,1.0,62,58.2,not_published
985 +Cloridorme,03010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,62,56.7,not_published
986 +Ogden,45020,Estrie,unifamilial,1.0,62,58.2,not_published
987 +Saint-Rosaire,39145,Centre-du-Québec,unifamilial,1.0,62,53.7,not_published
988 +Lac-Édouard,90027,Mauricie,unifamilial,1.0,62,58.2,not_published
989 +Saint-Patrice-de-Beaurivage,33025,Chaudière-Appalaches,unifamilial,1.0,62,55.2,not_published
990 +Dixville,44023,Estrie,unifamilial,1.5,62,59.7,not_published
991 +Saint-David,53005,Montérégie,unifamilial,1.0,61,52.2,not_published
992 +Westbury,41065,Estrie,unifamilial,1.0,61,62.7,not_published
993 +Saint-Paul,61005,Lanaudière,plex,1.5,61,50.7,not_published
994 +Longue-Rive,95032,Côte-Nord,unifamilial,1.0,61,58.2,not_published
995 +Saint-Denis-De La Bouteillerie,14055,Bas-Saint-Laurent,unifamilial,1.0,61,55.2,not_published
996 +Pointe-à-la-Croix,06030,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,61,61.2,not_published
997 +Saint-Stanislas,37245,Mauricie,unifamilial,1.0,61,61.2,not_published
998 +Mont-Saint-Hilaire,57035,Montérégie,plex,1.5,61,59.7,not_published
999 +Sainte-Hélène-de-Kamouraska,14025,Bas-Saint-Laurent,unifamilial,1.0,61,59.7,not_published
1000 +Lanoraie,52017,Lanaudière,plex,1.0,61,53.7,not_published
1001 +Saint-Bonaventure,49125,Centre-du-Québec,unifamilial,1.0,61,56.7,not_published
1002 +Saint-Cyprien,28040,Chaudière-Appalaches,unifamilial,1.5,61,53.7,not_published
1003 +Saint-Philippe,67010,Montérégie,condo,1.0,60,49.3,not_published
1004 +Métis-sur-Mer,09048,Bas-Saint-Laurent,unifamilial,1.0,60,62.7,not_published
1005 +Saint-Célestin,50030,Centre-du-Québec,unifamilial,1.0,60,56.7,not_published
1006 +Lac-Saint-Joseph,22015,Capitale-Nationale,unifamilial,1.0,60,50.7,not_published
1007 +L'Île-du-Grand-Calumet,84035,Outaouais,unifamilial,1.0,59,55.2,not_published
1008 +Arundel,78060,Laurentides,unifamilial,1.0,59,53.7,not_published
1009 +Alleyn-et-Cawood,84050,Outaouais,unifamilial,1.0,59,61.2,not_published
1010 +Clermont,15035,Capitale-Nationale,plex,1.0,59,56.7,not_published
1011 +Sainte-Julienne,63060,Lanaudière,plex,1.0,59,52.2,not_published
1012 +Saint-Frédéric,27065,Chaudière-Appalaches,unifamilial,1.0,59,52.2,not_published
1013 +Newport,41037,Estrie,unifamilial,1.0,59,62.7,not_published
1014 +Saint-Barnabé-Sud,54105,Montérégie,unifamilial,1.0,59,53.7,not_published
1015 +Shannon,22020,Capitale-Nationale,plex,1.0,59,55.2,not_published
1016 +Gallichan,87020,Abitibi-Témiscamingue,unifamilial,1.0,58,56.7,not_published
1017 +Bécancour,38010,Centre-du-Québec,condo,1.0,58,47.8,not_published
1018 +Fassett,80005,Outaouais,unifamilial,1.0,58,53.7,not_published
1019 +Montcerf-Lytton,83088,Outaouais,unifamilial,2.0,58,46.3,not_published
1020 +Ferland-et-Boilleau,94220,Saguenay–Lac-Saint-Jean,unifamilial,1.0,58,53.7,not_published
1021 +Sainte-Germaine-Boulé,87030,Abitibi-Témiscamingue,unifamilial,1.5,58,50.7,not_published
1022 +Sainte-Marthe,71110,Montérégie,unifamilial,1.0,58,56.7,not_published
1023 +Saint-Lazare-de-Bellechasse,19050,Chaudière-Appalaches,unifamilial,1.0,58,55.2,not_published
1024 +Taschereau,87042,Abitibi-Témiscamingue,unifamilial,1.0,57,53.7,not_published
1025 +Saint-Simon-de-Rimouski,11055,Bas-Saint-Laurent,unifamilial,1.0,57,55.2,not_published
1026 +Pointe-Calumet,72020,Laurentides,plex,1.5,57,50.7,not_published
1027 +Rivière-Rouge,79037,Laurentides,plex,1.0,56,52.2,not_published
1028 +Stanbridge East,46045,Estrie,unifamilial,1.0,56,55.2,not_published
1029 +Notre-Dame-du-Bon-Conseil,49080,Centre-du-Québec,unifamilial,1.0,56,50.7,not_published
1030 +Sainte-Rose-du-Nord,94230,Saguenay–Lac-Saint-Jean,unifamilial,1.0,56,62.7,not_published
1031 +Saint-Arsène,12065,Bas-Saint-Laurent,unifamilial,1.0,56,53.7,not_published
1032 +Sutton,46058,Estrie,condo,1.0,56,52.2,not_published
1033 +Packington,13015,Bas-Saint-Laurent,unifamilial,1.0,56,56.7,not_published
1034 +Grosses-Roches,08015,Bas-Saint-Laurent,unifamilial,1.0,56,58.2,not_published
1035 +Duhamel-Ouest,85030,Abitibi-Témiscamingue,unifamilial,1.0,55,55.2,not_published
1036 +Sainte-Christine,48020,Montérégie,unifamilial,1.0,55,65.7,not_published
1037 +Saint-Pierre-de-Broughton,31135,Chaudière-Appalaches,unifamilial,1.0,55,55.2,not_published
1038 +Saint-Nazaire-d'Acton,48050,Montérégie,unifamilial,1.0,55,52.2,not_published
1039 +Prévost,75040,Laurentides,condo,1.0,55,50.7,not_published
1040 +Saint-Jacques-de-Leeds,31140,Chaudière-Appalaches,unifamilial,1.0,55,53.7,not_published
1041 +Saint-Léon-le-Grand,51035,Mauricie,unifamilial,1.0,55,49.3,not_published
1042 +Courcelles–Saint-Évariste,29027,Chaudière-Appalaches,unifamilial,1.0,54,50.7,not_published
1043 +Métabetchouan–Lac-à-la-Croix,93012,Saguenay–Lac-Saint-Jean,plex,1.0,54,58.2,not_published
1044 +Amos,88057,Abitibi-Témiscamingue,plex,3.0,54,29.9,not_published
1045 +Saint-Vallier,19117,Chaudière-Appalaches,unifamilial,1.0,54,50.7,not_published
1046 +Saint-Charles-de-Bourget,94260,Saguenay–Lac-Saint-Jean,unifamilial,1.0,54,49.3,not_published
1047 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,plex,1.0,54,52.2,not_published
1048 +Saint-Édouard-de-Fabre,85015,Abitibi-Témiscamingue,unifamilial,1.0,54,56.7,not_published
1049 +Piopolis,30020,Estrie,unifamilial,1.0,53,53.7,not_published
1050 +Saint-Mathieu-d'Harricana,88050,Abitibi-Témiscamingue,unifamilial,1.0,53,49.3,not_published
1051 +Notre-Dame-du-Rosaire,18040,Chaudière-Appalaches,unifamilial,1.0,53,46.3,not_published
1052 +Pointe-Fortune,71140,Montérégie,unifamilial,1.0,53,53.7,not_published
1053 +La Pocatière,14082,Bas-Saint-Laurent,plex,1.0,53,58.2,not_published
1054 +Danville,40047,Estrie,plex,1.0,53,50.7,not_published
1055 +L'Ange-Gardien,82005,Outaouais,plex,1.0,53,53.7,not_published
1056 +Les Coteaux,71033,Montérégie,plex,1.0,53,49.3,not_published
1057 +Les Bergeronnes,95018,Côte-Nord,unifamilial,1.0,53,53.7,not_published
1058 +Sainte-Anne-des-Lacs,77035,Laurentides,plex,1.0,53,52.2,not_published
1059 +Chandler,02028,Gaspésie–Îles-de-la-Madeleine,plex,1.0,52,58.2,not_published
1060 +Sainte-Angèle-de-Prémont,51055,Mauricie,unifamilial,1.0,52,47.8,not_published
1061 +Saint-Pierre-de-la-Rivière-du-Sud,18055,Chaudière-Appalaches,unifamilial,1.0,52,52.2,not_published
1062 +Sainte-Martine,70012,Montérégie,condo,1.0,52,47.8,not_published
1063 +Saint-Just-de-Bretenières,18005,Chaudière-Appalaches,unifamilial,1.0,52,47.8,not_published
1064 +Saint-Sébastien,30085,Estrie,unifamilial,1.0,52,53.7,not_published
1065 +Saint-Michel-du-Squatec,13065,Bas-Saint-Laurent,unifamilial,2.0,52,40.3,not_published
1066 +Saint-André-Avellin,80027,Outaouais,plex,1.0,52,49.3,not_published
1067 +Saint-Zéphirin-de-Courval,50090,Centre-du-Québec,unifamilial,1.0,51,59.7,not_published
1068 +Lac-Beauport,22040,Capitale-Nationale,plex,1.0,51,55.2,not_published
1069 +Péribonka,92010,Saguenay–Lac-Saint-Jean,unifamilial,1.0,51,55.2,not_published
1070 +Mont-Royal,66072,Montréal,plex,1.0,51,50.7,not_published
1071 +Ville-Marie,85025,Abitibi-Témiscamingue,plex,1.0,51,53.7,not_published
1072 +Mercier,67045,Montérégie,condo,1.0,51,41.8,not_published
1073 +Parisville,38055,Centre-du-Québec,unifamilial,1.0,51,53.7,not_published
1074 +Saint-Edmond-de-Grantham,49100,Centre-du-Québec,unifamilial,1.0,51,50.7,not_published
1075 +Sainte-Sabine,28065,Chaudière-Appalaches,unifamilial,1.0,51,55.2,not_published
1076 +Clerval,87075,Abitibi-Témiscamingue,unifamilial,1.0,51,49.3,not_published
1077 +Saint-Basile-le-Grand,57020,Montérégie,plex,1.0,51,50.7,not_published
1078 +Beaupré,21025,Capitale-Nationale,plex,1.0,51,55.2,not_published
1079 +Disraeli,31015,Chaudière-Appalaches,plex,1.0,51,49.3,not_published
1080 +Val-Morin,78005,Laurentides,plex,1.0,51,47.8,not_published
1081 +Ulverton,42078,Estrie,unifamilial,2.0,50,43.3,not_published
1082 +Sainte-Angèle-de-Mérici,09035,Bas-Saint-Laurent,unifamilial,1.0,50,55.2,not_published
1083 +Notre-Dame-du-Sacré-Coeur-d'Issoudun,33085,Chaudière-Appalaches,unifamilial,1.0,50,55.2,not_published
1084 +Sainte-Françoise,11030,Bas-Saint-Laurent,unifamilial,1.0,50,46.3,not_published
1085 +Bryson,84025,Outaouais,unifamilial,1.0,50,55.2,not_published
1086 +Lac-des-Seize-Îles,77055,Laurentides,unifamilial,1.0,49,49.3,not_published
1087 +La Visitation-de-l'Île-Dupas,52050,Lanaudière,unifamilial,1.0,49,56.7,not_published
1088 +Saint-François-de-Sales,91015,Saguenay–Lac-Saint-Jean,unifamilial,1.0,49,49.3,not_published
1089 +Saint-Honoré-de-Témiscouata,13090,Bas-Saint-Laurent,unifamilial,1.0,49,52.2,not_published
1090 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,plex,1.0,49,49.3,not_published
1091 +Sainte-Anne-des-Plaines,73035,Laurentides,condo,2.0,49,34.3,not_published
1092 +Saint-Télesphore,71015,Montérégie,unifamilial,1.0,49,50.7,not_published
1093 +Waltham,84070,Outaouais,unifamilial,1.0,49,47.8,not_published
1094 +Scotstown,41080,Estrie,unifamilial,1.0,49,52.2,not_published
1095 +Saint-Apollinaire,33090,Chaudière-Appalaches,plex,1.0,49,44.8,not_published
1096 +Campbell's Bay,84030,Outaouais,unifamilial,1.0,49,58.2,not_published
1097 +Saint-Sulpice,60020,Lanaudière,condo,1.0,49,41.8,not_published
1098 +La Motte,88045,Abitibi-Témiscamingue,unifamilial,1.0,49,50.7,not_published
1099 +Saint-Eugène-de-Guigues,85085,Abitibi-Témiscamingue,unifamilial,1.0,49,50.7,not_published
1100 +Irlande,31040,Chaudière-Appalaches,unifamilial,1.0,49,44.8,not_published
1101 +Sainte-Anne-de-Bellevue,66117,Montréal,plex,1.0,49,43.3,not_published
1102 +Pontiac,82030,Outaouais,plex,1.0,48,52.2,not_published
1103 +East Farnham,46085,Estrie,unifamilial,1.0,48,55.2,not_published
1104 +Saint-Philippe-de-Néri,14060,Bas-Saint-Laurent,unifamilial,1.0,48,49.3,not_published
1105 +Saint-Roch-de-l'Achigan,63035,Lanaudière,plex,1.0,48,52.2,not_published
1106 +Murdochville,03025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,48,52.2,not_published
1107 +Saint-Honoré,94240,Saguenay–Lac-Saint-Jean,plex,1.0,48,52.2,not_published
1108 +Audet,30055,Estrie,unifamilial,1.0,48,55.2,not_published
1109 +Sainte-Brigitte-des-Saults,49085,Centre-du-Québec,unifamilial,1.0,48,53.7,not_published
1110 +Lochaber-Partie-Ouest,80060,Outaouais,unifamilial,1.0,48,49.3,not_published
1111 +Sainte-Marie-de-Blandford,38015,Centre-du-Québec,unifamilial,1.5,48,41.8,not_published
1112 +Shawinigan,36033,Mauricie,condo,1.0,48,44.8,not_published
1113 +Warwick,39077,Centre-du-Québec,plex,1.0,48,49.3,not_published
1114 +Beaconsfield,66107,Montréal,condo,1.0,47,49.3,not_published
1115 +Sainte-Euphémie-sur-Rivière-du-Sud,18035,Chaudière-Appalaches,unifamilial,1.0,47,50.7,not_published
1116 +Saint-Gabriel-Lalemant,14075,Bas-Saint-Laurent,unifamilial,1.0,47,52.2,not_published
1117 +Coteau-du-Lac,71040,Montérégie,plex,1.0,47,47.8,not_published
1118 +Saint-Prosper-de-Champlain,37250,Mauricie,unifamilial,1.0,47,46.3,not_published
1119 +Saint-Sylvère,38005,Centre-du-Québec,unifamilial,1.0,47,49.3,not_published
1120 +Stanstead,45008,Estrie,plex,1.0,47,53.7,not_published
1121 +Honfleur,19070,Chaudière-Appalaches,unifamilial,1.0,47,46.3,not_published
1122 +Sainte-Marie,26030,Chaudière-Appalaches,condo,1.0,47,49.3,not_published
1123 +Sainte-Séraphine,39105,Centre-du-Québec,unifamilial,1.0,47,49.3,not_published
1124 +Delson,67025,Montérégie,plex,1.0,47,46.3,not_published
1125 +Lawrenceville,42045,Estrie,unifamilial,1.0,47,47.8,not_published
1126 +Notre-Dame-de-Lourdes,32080,Centre-du-Québec,unifamilial,1.0,47,44.8,not_published
1127 +Sainte-Paule,08040,Bas-Saint-Laurent,unifamilial,1.0,47,47.8,not_published
1128 +Rivière-au-Tonnerre,98055,Côte-Nord,unifamilial,1.0,46,41.8,not_published
1129 +Senneville,66127,Montréal,unifamilial,1.0,46,49.3,not_published
1130 +Saints-Martyrs-Canadiens,39005,Centre-du-Québec,unifamilial,1.0,46,49.3,not_published
1131 +Les Coteaux,71033,Montérégie,condo,1.0,46,44.8,not_published
1132 +Tourville,17035,Chaudière-Appalaches,unifamilial,1.0,46,52.2,not_published
1133 +Saint-Alphonse,05065,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,46,47.8,not_published
1134 +Portneuf-sur-Mer,95040,Côte-Nord,unifamilial,1.0,46,47.8,not_published
1135 +Bedford,46035,Estrie,plex,1.0,46,43.3,not_published
1136 +Sainte-Louise,17060,Chaudière-Appalaches,unifamilial,1.0,46,43.3,not_published
1137 +Saint-Samuel,39130,Centre-du-Québec,unifamilial,1.0,46,53.7,not_published
1138 +Sainte-Martine,70012,Montérégie,plex,1.0,46,49.3,not_published
1139 +Sainte-Apolline-de-Patton,18025,Chaudière-Appalaches,unifamilial,1.0,45,46.3,not_published
1140 +Lac-Etchemin,28053,Chaudière-Appalaches,plex,1.0,45,50.7,not_published
1141 +Tadoussac,95005,Côte-Nord,unifamilial,1.0,45,46.3,not_published
1142 +Sainte-Perpétue,50050,Centre-du-Québec,unifamilial,1.0,45,43.3,not_published
1143 +Saint-André-du-Lac-Saint-Jean,91010,Saguenay–Lac-Saint-Jean,unifamilial,1.0,45,43.3,not_published
1144 +Saint-Lazare,71105,Montérégie,plex,1.0,45,46.3,not_published
1145 +Saint-Mathias-sur-Richelieu,55065,Montérégie,plex,1.0,45,52.2,not_published
1146 +Sainte-Anne-de-la-Rochelle,42050,Estrie,unifamilial,1.0,44,40.3,not_published
1147 +Saint-Adalbert,17015,Chaudière-Appalaches,unifamilial,1.0,44,46.3,not_published
1148 +Saint-Malo,44003,Estrie,unifamilial,1.0,44,47.8,not_published
1149 +Ham-Sud,40005,Estrie,unifamilial,1.0,44,47.8,not_published
1150 +Les Cèdres,71050,Montérégie,condo,1.0,44,44.8,not_published
1151 +Baie-des-Sables,08080,Bas-Saint-Laurent,unifamilial,1.0,44,49.3,not_published
1152 +Petit-Saguenay,94205,Saguenay–Lac-Saint-Jean,unifamilial,1.0,44,40.3,not_published
1153 +Saint-Marc-des-Carrières,34065,Capitale-Nationale,plex,1.0,44,49.3,not_published
1154 +Saint-Sébastien,56050,Montérégie,unifamilial,1.0,44,46.3,not_published
1155 +Otterburn Park,57030,Montérégie,plex,1.0,44,37.3,not_published
1156 +Ormstown,69037,Montérégie,plex,1.0,43,47.8,not_published
1157 +Château-Richer,21035,Capitale-Nationale,plex,1.0,43,43.3,not_published
1158 +Stornoway,30105,Estrie,unifamilial,1.0,43,47.8,not_published
1159 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,plex,1.0,43,47.8,not_published
1160 +Sainte-Famille-de-l'Île-d'Orléans,20010,Capitale-Nationale,unifamilial,1.0,43,46.3,not_published
1161 +Lac-Brome,46075,Estrie,plex,1.0,43,43.3,not_published
1162 +Saint-Joseph-du-Lac,72025,Laurentides,condo,2.0,43,29.9,not_published
1163 +Huntingdon,69055,Montérégie,plex,1.0,43,41.8,not_published
1164 +Baie-Trinité,96005,Côte-Nord,unifamilial,1.0,43,46.3,not_published
1165 +Senneterre,89040,Abitibi-Témiscamingue,plex,1.0,43,49.3,not_published
1166 +Verchères,59025,Montérégie,plex,1.0,43,44.8,not_published
1167 +Candiac,67020,Montérégie,plex,1.0,43,49.3,not_published
1168 +Biencourt,13055,Bas-Saint-Laurent,unifamilial,1.0,43,40.3,not_published
1169 +Saint-Augustin-de-Woburn,30005,Estrie,unifamilial,1.0,43,49.3,not_published
1170 +Havre-Saint-Pierre,98040,Côte-Nord,plex,1.0,43,49.3,not_published
1171 +Rawdon,62037,Lanaudière,condo,1.0,43,40.3,not_published
1172 +Saint-Paul-de-la-Croix,12035,Bas-Saint-Laurent,unifamilial,1.0,42,46.3,not_published
1173 +Rouyn-Noranda,86042,Abitibi-Témiscamingue,condo,1.0,42,40.3,not_published
1174 +Saint-Jean-de-Matha,62015,Lanaudière,plex,1.0,42,41.8,not_published
1175 +Saint-Luc-de-Vincennes,37225,Mauricie,unifamilial,1.0,42,43.3,not_published
1176 +Portneuf,34048,Capitale-Nationale,plex,1.0,42,43.3,not_published
1177 +Rosemère,73020,Laurentides,plex,1.0,42,38.8,not_published
1178 +Saint-Marcel,17020,Chaudière-Appalaches,unifamilial,1.0,42,38.8,not_published
1179 +Weedon,41098,Estrie,plex,1.0,42,43.3,not_published
1180 +Notre-Dame-de-Lourdes,61045,Lanaudière,plex,1.0,42,46.3,not_published
1181 +Pike River,46025,Estrie,unifamilial,1.0,42,44.8,not_published
1182 +Sainte-Gertrude-Manneville,88085,Abitibi-Témiscamingue,unifamilial,1.0,41,46.3,not_published
1183 +Cowansville,46080,Estrie,condo,1.0,41,44.8,not_published
1184 +Saint-Joseph-de-Lepage,09070,Bas-Saint-Laurent,unifamilial,1.0,41,44.8,not_published
1185 +Saint-Adrien,40010,Estrie,unifamilial,1.0,41,40.3,not_published
1186 +Forestville,95045,Côte-Nord,plex,1.0,41,46.3,not_published
1187 +Brome,46070,Estrie,unifamilial,1.0,41,43.3,not_published
1188 +Leclercville,33123,Chaudière-Appalaches,unifamilial,1.0,41,40.3,not_published
1189 +Notre-Dame-de-Ham,39015,Centre-du-Québec,unifamilial,1.0,41,44.8,not_published
1190 +Saint-Henri,19068,Chaudière-Appalaches,plex,1.0,41,49.3,not_published
1191 +Roxton Pond,47047,Estrie,plex,1.0,41,41.8,not_published
1192 +Saint-Adelme,08030,Bas-Saint-Laurent,unifamilial,1.0,41,46.3,not_published
1193 +Montréal-Ouest,66047,Montréal,plex,1.0,41,44.8,not_published
1194 +Saint-Simon-les-Mines,29125,Chaudière-Appalaches,unifamilial,1.0,40,47.8,not_published
1195 +Hampstead,66062,Montréal,plex,1.0,40,49.3,not_published
1196 +Boischatel,21045,Capitale-Nationale,plex,1.0,40,44.8,not_published
1197 +Saint-Moïse,07095,Bas-Saint-Laurent,unifamilial,1.0,40,41.8,not_published
1198 +Dundee,69075,Montérégie,unifamilial,1.0,40,40.3,not_published
1199 +Oka,72032,Laurentides,plex,1.0,40,43.3,not_published
1200 +Saint-Sixte,80070,Outaouais,unifamilial,1.0,40,41.8,not_published
1201 +Bonaventure,05045,Gaspésie–Îles-de-la-Madeleine,plex,1.0,40,41.8,not_published
1202 +Saint-Adrien-d'Irlande,31095,Chaudière-Appalaches,unifamilial,2.0,39,34.3,not_published
1203 +Barnston-Ouest,44045,Estrie,unifamilial,1.0,39,47.8,not_published
1204 +Saint-Joseph-de-Beauce,27043,Chaudière-Appalaches,plex,1.0,39,41.8,not_published
1205 +Bedford,46040,Estrie,unifamilial,1.0,39,38.8,not_published
1206 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,plex,1.0,39,46.3,not_published
1207 +Saint-Louis-de-Gonzague,28035,Chaudière-Appalaches,unifamilial,1.0,39,47.8,not_published
1208 +Saint-Camille,40025,Estrie,unifamilial,1.0,39,43.3,not_published
1209 +Normétal,87115,Abitibi-Témiscamingue,unifamilial,1.0,38,41.8,not_published
1210 +Lingwick,41085,Estrie,unifamilial,1.0,38,46.3,not_published
1211 +Saint-Côme,62065,Lanaudière,plex,1.0,38,37.3,not_published
1212 +Saint-Lazare,71105,Montérégie,condo,1.0,38,31.3,not_published
1213 +Grenville,76055,Laurentides,plex,1.0,38,44.8,not_published
1214 +Rivière-Éternité,94215,Saguenay–Lac-Saint-Jean,unifamilial,1.0,38,44.8,not_published
1215 +Roquemaure,87015,Abitibi-Témiscamingue,unifamilial,1.0,38,44.8,not_published
1216 +Villeroy,32085,Centre-du-Québec,unifamilial,1.0,38,40.3,not_published
1217 +Dégelis,13005,Bas-Saint-Laurent,plex,1.0,38,41.8,not_published
1218 +Cascapédia–Saint-Jules,05077,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,38,40.3,not_published
1219 +Saint-Germain-de-Grantham,49048,Centre-du-Québec,plex,1.0,38,43.3,not_published
1220 +Sainte-Thècle,35050,Mauricie,plex,1.0,38,41.8,not_published
1221 +Saint-François-de-l'Île-d'Orléans,20005,Capitale-Nationale,unifamilial,1.0,38,43.3,not_published
1222 +Papineauville,80037,Outaouais,plex,1.0,38,37.3,not_published
1223 +Stanstead-Est,44050,Estrie,unifamilial,1.0,38,41.8,not_published
1224 +Pohénégamook,13095,Bas-Saint-Laurent,plex,1.0,37,46.3,not_published
1225 +Saint-Valentin,56030,Montérégie,unifamilial,1.0,37,40.3,not_published
1226 +Saint-Denis-de-Brompton,42025,Estrie,plex,1.0,37,38.8,not_published
1227 +Val-d'Or,89008,Abitibi-Témiscamingue,condo,1.0,37,49.3,not_published
1228 +Saint-Philippe,67010,Montérégie,plex,1.0,37,43.3,not_published
1229 +Fortierville,38047,Centre-du-Québec,unifamilial,1.0,37,38.8,not_published
1230 +Saint-Célestin,50035,Centre-du-Québec,unifamilial,1.0,37,43.3,not_published
1231 +Abercorn,46005,Estrie,unifamilial,1.0,37,41.8,not_published
1232 +Martinville,44060,Estrie,unifamilial,1.0,37,41.8,not_published
1233 +Kinnear's Mills,31105,Chaudière-Appalaches,unifamilial,1.0,37,38.8,not_published
1234 +Saint-Ambroise-de-Kildare,61040,Lanaudière,plex,1.0,37,38.8,not_published
1235 +Saint-Marc-du-Lac-Long,13020,Bas-Saint-Laurent,unifamilial,1.0,36,37.3,not_published
1236 +Notre-Dame-de-Bonsecours,80015,Outaouais,unifamilial,1.0,36,40.3,not_published
1237 +Latulipe-et-Gaboury,85060,Abitibi-Témiscamingue,unifamilial,1.0,36,46.3,not_published
1238 +Saint-Félix-de-Valois,62007,Lanaudière,condo,1.0,36,35.8,not_published
1239 +Saint-François-d'Assise,06055,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,36,34.3,not_published
1240 +Poularies,87035,Abitibi-Témiscamingue,unifamilial,1.0,36,43.3,not_published
1241 +Rémigny,85105,Abitibi-Témiscamingue,unifamilial,1.0,36,34.3,not_published
1242 +Alma,93042,Saguenay–Lac-Saint-Jean,condo,1.0,36,40.3,not_published
1243 +Neuville,34007,Capitale-Nationale,plex,1.0,36,40.3,not_published
1244 +Dorval,66087,Montréal,plex,1.0,35,37.3,not_published
1245 +Notre-Dame-de-Stanbridge,46100,Estrie,unifamilial,1.0,35,43.3,not_published
1246 +Kamouraska,14050,Bas-Saint-Laurent,unifamilial,1.0,35,40.3,not_published
1247 +Saint-Thuribe,34085,Capitale-Nationale,unifamilial,1.0,35,40.3,not_published
1248 +Sainte-Hélène-de-Mancebourg,87070,Abitibi-Témiscamingue,unifamilial,1.0,35,34.3,not_published
1249 +Saint-Cyrille-de-Wendover,49070,Centre-du-Québec,plex,1.0,35,41.8,not_published
1250 +Valcourt,42055,Estrie,plex,1.0,35,37.3,not_published
1251 +Saint-Thomas,61027,Lanaudière,plex,1.0,35,35.8,not_published
1252 +Saint-Alfred,27015,Chaudière-Appalaches,unifamilial,1.0,35,46.3,not_published
1253 +Saint-Marcel-de-Richelieu,54125,Montérégie,unifamilial,1.0,35,37.3,not_published
1254 +Nédélec,85100,Abitibi-Témiscamingue,unifamilial,1.0,35,35.8,not_published
1255 +Sainte-Madeleine,54025,Montérégie,plex,1.0,34,40.3,not_published
1256 +Milan,30040,Estrie,unifamilial,1.0,34,41.8,not_published
1257 +Terrasse-Vaudreuil,71075,Montérégie,plex,1.0,34,40.3,not_published
1258 +La Conception,78115,Laurentides,plex,1.0,34,35.8,not_published
1259 +Matapédia,06045,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,34,37.3,not_published
1260 +Gracefield,83032,Outaouais,plex,1.0,34,40.3,not_published
1261 +Saint-Stanislas,92070,Saguenay–Lac-Saint-Jean,unifamilial,1.0,34,40.3,not_published
1262 +Saint-Bruno-de-Kamouraska,14010,Bas-Saint-Laurent,unifamilial,1.0,34,34.3,not_published
1263 +Saint-Octave-de-Métis,09055,Bas-Saint-Laurent,unifamilial,1.0,33,38.8,not_published
1264 +Les Hauteurs,09015,Bas-Saint-Laurent,unifamilial,1.0,33,46.3,not_published
1265 +Lac-des-Aigles,13062,Bas-Saint-Laurent,unifamilial,2.0,33,26.9,not_published
1266 +Saint-Clément,11005,Bas-Saint-Laurent,unifamilial,1.0,33,38.8,not_published
1267 +Lebel-sur-Quévillon,99005,Nord-du-Québec,plex,1.0,33,34.3,not_published
1268 +Saint-Damase-de-L'Islet,17040,Chaudière-Appalaches,unifamilial,1.0,33,37.3,not_published
1269 +Sainte-Anne-de-la-Pérade,37205,Mauricie,plex,1.0,33,38.8,not_published
1270 +Escuminac,06025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,33,38.8,not_published
1271 +Berry,88070,Abitibi-Témiscamingue,unifamilial,1.0,32,37.3,not_published
1272 +Lochaber,80055,Outaouais,unifamilial,1.0,32,40.3,not_published
1273 +Chichester,84090,Outaouais,unifamilial,1.0,32,35.8,not_published
1274 +Saint-Omer,17005,Chaudière-Appalaches,unifamilial,1.0,32,38.8,not_published
1275 +Colombier,95050,Côte-Nord,unifamilial,1.0,32,38.8,not_published
1276 +Saint-André-de-Kamouraska,14040,Bas-Saint-Laurent,unifamilial,1.0,32,40.3,not_published
1277 +Saint-Adolphe-d'Howard,77065,Laurentides,plex,1.0,32,40.3,not_published
1278 +Sainte-Agathe-des-Monts,78032,Laurentides,condo,1.0,32,37.3,not_published
1279 +Notre-Dame-des-Monts,15025,Capitale-Nationale,unifamilial,1.0,32,38.8,not_published
1280 +Sainte-Irène,07040,Bas-Saint-Laurent,unifamilial,1.0,32,37.3,not_published
1281 +Kipawa,85010,Abitibi-Témiscamingue,unifamilial,1.0,31,32.8,not_published
1282 +Egan-Sud,83075,Outaouais,unifamilial,1.0,31,41.8,not_published
1283 +Victoriaville,39062,Centre-du-Québec,condo,1.0,31,32.8,not_published
1284 +Maricourt,42065,Estrie,unifamilial,1.0,31,32.8,not_published
1285 +Moffet,85075,Abitibi-Témiscamingue,unifamilial,1.0,31,40.3,not_published
1286 +Macamic,87058,Abitibi-Témiscamingue,plex,1.0,31,37.3,not_published
1287 +Saint-Jean-de-la-Lande,13010,Bas-Saint-Laurent,unifamilial,1.0,31,37.3,not_published
1288 +Grand-Métis,09060,Bas-Saint-Laurent,unifamilial,1.0,31,38.8,not_published
1289 +Lac-Supérieur,78095,Laurentides,plex,1.0,31,38.8,not_published
1290 +Franquelin,96015,Côte-Nord,unifamilial,1.0,31,34.3,not_published
1291 +Saint-Bernard-de-Michaudville,54115,Montérégie,unifamilial,1.0,31,34.3,not_published
1292 +Saint-Elzéar,05050,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,31,35.8,not_published
1293 +Saint-Dominique-du-Rosaire,88065,Abitibi-Témiscamingue,unifamilial,1.0,30,37.3,not_published
1294 +Lacolle,56023,Montérégie,plex,1.0,30,32.8,not_published
1295 +Saint-Vianney,07075,Bas-Saint-Laurent,unifamilial,1.0,30,32.8,not_published
1296 +Saint-Boniface,51085,Mauricie,plex,1.0,30,37.3,not_published
1297 +Saint-Jules,27055,Chaudière-Appalaches,unifamilial,1.0,30,35.8,not_published
1298 +Clermont,87110,Abitibi-Témiscamingue,unifamilial,1.0,30,37.3,not_published
1299 +Saint-Roch-de-Richelieu,53040,Montérégie,plex,1.0,30,37.3,not_published
1300 +Saint-Jean-Baptiste,57033,Montérégie,plex,1.0,30,37.3,not_published
1301 +Chartierville,41020,Estrie,unifamilial,1.0,30,34.3,not_published
1302 +La Trinité-des-Monts,10010,Bas-Saint-Laurent,unifamilial,1.0,30,37.3,not_published
1303 +Maddington Falls,39165,Centre-du-Québec,unifamilial,1.0,30,35.8,not_published
1304 +Val-Racine,30015,Estrie,unifamilial,1.0,30,32.8,not_published
1305 +Saint-Sulpice,60020,Lanaudière,plex,1.0,29,34.3,not_published
1306 +Sainte-Sophie-d'Halifax,32023,Centre-du-Québec,unifamilial,1.0,29,38.8,not_published
1307 +Saint-Robert-Bellarmin,30070,Estrie,unifamilial,1.0,29,31.3,not_published
1308 +Elgin,69050,Montérégie,unifamilial,1.0,29,32.8,not_published
1309 +Saint-Paul,61005,Lanaudière,condo,1.0,29,32.8,not_published
1310 +Saint-François-Xavier-de-Viger,12025,Bas-Saint-Laurent,unifamilial,1.0,29,31.3,not_published
1311 +Port-Cartier,97022,Côte-Nord,plex,1.0,29,35.8,not_published
1312 +Guérin,85095,Abitibi-Témiscamingue,unifamilial,1.0,29,31.3,not_published
1313 +Saint-Ignace-de-Stanbridge,46095,Estrie,unifamilial,1.0,29,37.3,not_published
1314 +Saint-Anselme,19062,Chaudière-Appalaches,plex,1.0,29,38.8,not_published
1315 +Ferme-Neuve,79097,Laurentides,plex,1.0,29,29.9,not_published
1316 +Sainte-Thérèse-de-Gaspé,02010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,29,34.3,not_published
1317 +Saint-Nazaire-de-Dorchester,19015,Chaudière-Appalaches,unifamilial,1.0,28,34.3,not_published
1318 +Blanc-Sablon,98005,Côte-Nord,unifamilial,1.0,28,38.8,not_published
1319 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,plex,1.0,28,31.3,not_published
1320 +Saint-Eugène-d'Argentenay,92065,Saguenay–Lac-Saint-Jean,unifamilial,1.0,28,34.3,not_published
1321 +Barkmere,78050,Laurentides,unifamilial,1.0,28,34.3,not_published
1322 +Saint-Ferréol-les-Neiges,21010,Capitale-Nationale,plex,1.0,28,28.4,not_published
1323 +Ascot Corner,41055,Estrie,plex,1.0,28,31.3,not_published
1324 +Sainte-Mélanie,61050,Lanaudière,plex,1.0,28,34.3,not_published
1325 +Warden,47030,Estrie,unifamilial,1.0,28,34.3,not_published
1326 +La Rédemption,09005,Bas-Saint-Laurent,unifamilial,1.0,28,35.8,not_published
1327 +Saint-Eusèbe,13030,Bas-Saint-Laurent,unifamilial,1.0,28,31.3,not_published
1328 +Saint-Alphonse-Rodriguez,62025,Lanaudière,plex,1.0,27,32.8,not_published
1329 +Lac-des-Écorces,79078,Laurentides,plex,1.0,27,26.9,not_published
1330 +Bois-Franc,83085,Outaouais,unifamilial,1.0,27,32.8,not_published
1331 +Lac-Frontière,18010,Chaudière-Appalaches,unifamilial,1.0,27,34.3,not_published
1332 +Chelsea,82025,Outaouais,plex,1.0,27,35.8,not_published
1333 +Saint-Agapit,33045,Chaudière-Appalaches,plex,1.0,27,32.8,not_published
1334 +Laurier-Station,33060,Chaudière-Appalaches,plex,1.0,27,31.3,not_published
1335 +Saint-François-du-Lac,50128,Centre-du-Québec,plex,1.0,27,29.9,not_published
1336 +Sainte-Edwidge-de-Clifton,44055,Estrie,unifamilial,1.0,27,28.4,not_published
1337 +Saint-Pie-de-Guire,49130,Centre-du-Québec,unifamilial,1.0,27,34.3,not_published
1338 +Saint-Ambroise,94255,Saguenay–Lac-Saint-Jean,plex,1.0,27,32.8,not_published
1339 +Sainte-Madeleine-de-la-Rivière-Madeleine,04005,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,27,35.8,not_published
1340 +Godbout,96010,Côte-Nord,unifamilial,1.0,27,34.3,not_published
1341 +Saint-Alexis-de-Matapédia,06050,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,27,34.3,not_published
1342 +Maskinongé,51008,Mauricie,plex,1.0,27,37.3,not_published
1343 +Saint-Prosper,28020,Chaudière-Appalaches,plex,1.0,27,34.3,not_published
1344 +Saint-Bruno-de-Montarville,58037,Montérégie,plex,1.0,27,35.8,not_published
1345 +Esprit-Saint,10005,Bas-Saint-Laurent,unifamilial,1.0,27,34.3,not_published
1346 +Saint-Augustin,92005,Saguenay–Lac-Saint-Jean,unifamilial,1.0,27,38.8,not_published
1347 +Notre-Dame-du-Mont-Carmel,37235,Mauricie,plex,1.0,27,35.8,not_published
1348 +Saint-Éloi,11035,Bas-Saint-Laurent,unifamilial,1.0,27,26.9,not_published
1349 +Saint-Aimé,53015,Montérégie,unifamilial,1.0,26,28.4,not_published
1350 +Longue-Pointe-de-Mingan,98045,Côte-Nord,unifamilial,1.0,26,31.3,not_published
1351 +Saint-Casimir,34078,Capitale-Nationale,plex,1.0,26,26.9,not_published
1352 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,plex,1.0,26,29.9,not_published
1353 +Waterville,44080,Estrie,plex,1.0,26,29.9,not_published
1354 +Sainte-Claire,19055,Chaudière-Appalaches,plex,1.0,26,31.3,not_published
1355 +Otterburn Park,57030,Montérégie,condo,1.0,26,25.4,not_published
1356 +Yamachiche,51020,Mauricie,plex,1.0,26,37.3,not_published
1357 +Sainte-Croix,33102,Chaudière-Appalaches,plex,1.0,26,26.9,not_published
1358 +Daveluyville,39152,Centre-du-Québec,plex,1.0,26,32.8,not_published
1359 +Saint-Antoine-de-l'Isle-aux-Grues,18070,Chaudière-Appalaches,unifamilial,1.0,26,32.8,not_published
1360 +Saint-Zénon-du-Lac-Humqui,07035,Bas-Saint-Laurent,unifamilial,1.0,26,28.4,not_published
1361 +East Broughton,31122,Chaudière-Appalaches,plex,1.0,26,29.9,not_published
1362 +Sainte-Luce,09092,Bas-Saint-Laurent,plex,1.0,26,31.3,not_published
1363 +Sainte-Félicité,17025,Chaudière-Appalaches,unifamilial,1.0,26,31.3,not_published
1364 +Saint-Nazaire,93045,Saguenay–Lac-Saint-Jean,plex,1.0,26,32.8,not_published
1365 +Barraute,88022,Abitibi-Témiscamingue,plex,1.0,26,31.3,not_published
1366 +Saint-Alexis-des-Monts,51065,Mauricie,plex,1.0,26,26.9,not_published
1367 +Portage-du-Fort,84020,Outaouais,unifamilial,1.0,26,29.9,not_published
1368 +Calixa-Lavallée,59030,Montérégie,unifamilial,1.0,25,29.9,not_published
1369 +Saint-Julien,31035,Chaudière-Appalaches,unifamilial,1.0,25,32.8,not_published
1370 +Saint-Léandre,08065,Bas-Saint-Laurent,unifamilial,1.0,25,31.3,not_published
1371 +Saint-Esprit,63030,Lanaudière,plex,1.0,25,31.3,not_published
1372 +Cap-Saint-Ignace,18045,Chaudière-Appalaches,plex,1.0,25,29.9,not_published
1373 +L'Islet,17078,Chaudière-Appalaches,plex,1.0,25,26.9,not_published
1374 +McMasterville,57025,Montérégie,condo,1.0,25,29.9,not_published
1375 +Orford,45115,Estrie,plex,1.0,25,31.3,not_published
1376 +Hope,05025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,25,31.3,not_published
1377 +Saint-Séverin,27070,Chaudière-Appalaches,unifamilial,1.0,25,34.3,not_published
1378 +La Pêche,82035,Outaouais,condo,1.0,25,29.9,not_published
1379 +Saint-Noël,07100,Bas-Saint-Laurent,unifamilial,1.0,25,29.9,not_published
1380 +Deschambault-Grondines,34058,Capitale-Nationale,plex,1.0,24,34.3,not_published
1381 +New Richmond,05070,Gaspésie–Îles-de-la-Madeleine,plex,1.0,24,31.3,not_published
1382 +Potton,45030,Estrie,condo,1.0,24,29.9,not_published
1383 +Ange-Gardien,55008,Montérégie,plex,1.0,24,29.9,not_published
1384 +Saint-Tharcisius,07070,Bas-Saint-Laurent,unifamilial,1.0,24,29.9,not_published
1385 +Sainte-Monique,50057,Centre-du-Québec,unifamilial,1.0,24,26.9,not_published
1386 +Chazel,87095,Abitibi-Témiscamingue,unifamilial,1.0,24,28.4,not_published
1387 +Sainte-Hélène-de-Chester,39035,Centre-du-Québec,unifamilial,1.0,24,31.3,not_published
1388 +Mont-Saint-Grégoire,56097,Montérégie,plex,1.0,24,25.4,not_published
1389 +Aston-Jonction,50013,Centre-du-Québec,unifamilial,1.5,24,23.9,not_published
1390 +Lyster,32065,Centre-du-Québec,plex,1.0,24,28.4,not_published
1391 +Yamaska,53072,Montérégie,plex,1.0,24,29.9,not_published
1392 +Ivry-sur-le-Lac,78042,Laurentides,unifamilial,1.0,24,31.3,not_published
1393 +Pointe-Claire,66097,Montréal,plex,1.0,24,31.3,not_published
1394 +Saint-André-d'Argenteuil,76008,Laurentides,plex,1.0,23,28.4,not_published
1395 +Saint-Pierre-de-Lamy,13075,Bas-Saint-Laurent,unifamilial,1.0,23,31.3,not_published
1396 +Saint-Eugène-de-Ladrière,10075,Bas-Saint-Laurent,unifamilial,1.0,23,29.9,not_published
1397 +Notre-Dame-du-Laus,79005,Laurentides,plex,1.0,23,28.4,not_published
1398 +Sainte-Marie-Madeleine,54030,Montérégie,plex,1.0,23,25.4,not_published
1399 +Saint-Paulin,51060,Mauricie,plex,1.0,23,25.4,not_published
1400 +Sainte-Clotilde-de-Beauce,31060,Chaudière-Appalaches,unifamilial,1.0,23,25.4,not_published
1401 +Sainte-Lucie-de-Beauregard,18020,Chaudière-Appalaches,unifamilial,1.0,23,28.4,not_published
1402 +Sainte-Florence,07010,Bas-Saint-Laurent,unifamilial,1.0,23,26.9,not_published
1403 +Saint-Sévère,51030,Mauricie,unifamilial,1.0,23,25.4,not_published
1404 +Authier-Nord,87100,Abitibi-Témiscamingue,unifamilial,1.0,23,32.8,not_published
1405 +Grand-Saint-Esprit,50065,Centre-du-Québec,unifamilial,1.0,23,29.9,not_published
1406 +Saint-Gabriel-de-Valcartier,22025,Capitale-Nationale,plex,1.0,23,26.9,not_published
1407 +Saint-Anaclet-de-Lessard,10030,Bas-Saint-Laurent,plex,1.0,23,23.9,not_published
1408 +Saint-Antonin,12015,Bas-Saint-Laurent,plex,1.0,23,29.9,not_published
1409 +Béthanie,48005,Montérégie,unifamilial,1.0,23,29.9,not_published
1410 +Saint-Jean-Port-Joli,17070,Chaudière-Appalaches,plex,1.0,23,28.4,not_published
1411 +Normandin,92040,Saguenay–Lac-Saint-Jean,plex,1.0,23,31.3,not_published
1412 +Saint-Barthélemy,52055,Lanaudière,plex,1.0,23,31.3,not_published
1413 +Sacré-Coeur-de-Jésus,31130,Chaudière-Appalaches,unifamilial,1.0,23,31.3,not_published
1414 +Sainte-Françoise,38035,Centre-du-Québec,unifamilial,1.0,23,31.3,not_published
1415 +Saint-Henri,19068,Chaudière-Appalaches,condo,1.0,23,23.9,not_published
1416 +Sainte-Cécile-de-Lévrard,38060,Centre-du-Québec,unifamilial,1.0,22,28.4,not_published
1417 +East Hereford,44010,Estrie,unifamilial,1.0,22,25.4,not_published
1418 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,plex,1.0,22,31.3,not_published
1419 +Beauharnois,70022,Montérégie,condo,1.0,22,25.4,not_published
1420 +Rivière-du-Loup,12072,Bas-Saint-Laurent,condo,1.0,22,23.9,not_published
1421 +Rapide-Danseur,87010,Abitibi-Témiscamingue,unifamilial,1.0,22,26.9,not_published
1422 +Roxton Falls,48010,Montérégie,plex,1.0,22,31.3,not_published
1423 +Les Méchins,08005,Bas-Saint-Laurent,unifamilial,2.0,22,19.4,not_published
1424 +La Présentation,54035,Montérégie,plex,1.0,22,28.4,not_published
1425 +Wickham,49040,Centre-du-Québec,plex,1.0,22,23.9,not_published
1426 +Sainte-Justine,28045,Chaudière-Appalaches,plex,1.0,22,32.8,not_published
1427 +Marsoui,04025,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,22,26.9,not_published
1428 +Eastman,45093,Estrie,plex,1.0,22,26.9,not_published
1429 +Saint-Apollinaire,33090,Chaudière-Appalaches,condo,1.0,22,19.4,not_published
1430 +Dollard-des-Ormeaux,66142,Montréal,plex,1.0,22,29.9,not_published
1431 +Sainte-Élisabeth,52030,Lanaudière,plex,1.0,22,25.4,not_published
1432 +Lac-Tremblant-Nord,78127,Laurentides,unifamilial,1.0,21,28.4,not_published
1433 +Rigaud,71133,Montérégie,condo,1.0,21,28.4,not_published
1434 +Sainte-Catherine,67030,Montérégie,condo,1.0,21,19.4,not_published
1435 +Napierville,68030,Montérégie,plex,1.0,21,23.9,not_published
1436 +Sainte-Rita,11015,Bas-Saint-Laurent,unifamilial,1.0,21,19.4,not_published
1437 +Sainte-Barbe,69065,Montérégie,plex,1.0,21,26.9,not_published
1438 +Amherst,78070,Laurentides,plex,1.0,21,25.4,not_published
1439 +Saint-Raphaël,19082,Chaudière-Appalaches,plex,1.0,21,29.9,not_published
1440 +Saint-Alexandre-de-Kamouraska,14035,Bas-Saint-Laurent,plex,1.0,21,26.9,not_published
1441 +Cap-Santé,34030,Capitale-Nationale,plex,1.0,21,26.9,not_published
1442 +Saint-Prime,91035,Saguenay–Lac-Saint-Jean,plex,1.0,21,26.9,not_published
1443 +Saint-Jean-de-Brébeuf,31100,Chaudière-Appalaches,unifamilial,1.0,21,25.4,not_published
1444 +Saint-Lin–Laurentides,63048,Lanaudière,condo,1.0,21,20.9,not_published
1445 +Saint-Martin,29045,Chaudière-Appalaches,plex,1.0,21,25.4,not_published
1446 +Témiscaming,85005,Abitibi-Témiscamingue,plex,1.0,21,25.4,not_published
1447 +La Reine,87080,Abitibi-Témiscamingue,unifamilial,1.0,21,28.4,not_published
1448 +Rougemont,55037,Montérégie,plex,1.0,21,23.9,not_published
1449 +Padoue,09040,Bas-Saint-Laurent,unifamilial,1.0,21,23.9,not_published
1450 +Chénéville,80103,Outaouais,plex,1.0,21,23.9,not_published
1451 +Fugèreville,85055,Abitibi-Témiscamingue,unifamilial,1.0,21,28.4,not_published
1452 +Stoke,42005,Estrie,plex,1.0,21,26.9,not_published
1453 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,plex,1.0,21,26.9,not_published
1454 +Saint-Fortunat,31030,Chaudière-Appalaches,unifamilial,1.0,21,25.4,not_published
1455 +Sainte-Élizabeth-de-Warwick,39090,Centre-du-Québec,unifamilial,1.0,21,26.9,not_published
1456 +Saint-Maurice,37230,Mauricie,plex,1.0,20,22.4,not_published
1457 +Saint-Joachim,21020,Capitale-Nationale,plex,1.0,20,25.4,not_published
1458 +Saint-Fulgence,94235,Saguenay–Lac-Saint-Jean,plex,1.0,20,25.4,not_published
1459 +Les Cèdres,71050,Montérégie,plex,1.0,20,23.9,not_published
1460 +Saint-Basile,34038,Capitale-Nationale,plex,1.0,20,26.9,not_published
1461 +Saint-Gérard-Majella,53085,Montérégie,unifamilial,1.0,20,22.4,not_published
1462 +Caplan,05060,Gaspésie–Îles-de-la-Madeleine,plex,1.0,20,28.4,not_published
1463 +Baie-Sainte-Catherine,15065,Capitale-Nationale,unifamilial,1.0,20,25.4,not_published
1464 +Cacouna,12057,Bas-Saint-Laurent,plex,1.0,20,28.4,not_published
1465 +Lavaltrie,52007,Lanaudière,condo,1.0,20,28.4,not_published
1466 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,plex,1.0,20,22.4,not_published
1467 +Sainte-Victoire-de-Sorel,53025,Montérégie,plex,1.0,20,28.4,not_published
1468 +Laniel,85905,Abitibi-Témiscamingue,unifamilial,1.0,20,25.4,not_published
1469 +Lac-Normand,35904,Mauricie,unifamilial,1.0,20,26.9,not_published
1470 +Sainte-Jeanne-d'Arc-de-la-Mitis,09020,Bas-Saint-Laurent,unifamilial,1.0,20,29.9,not_published
1471 +Saint-Charles-de-Bellechasse,19097,Chaudière-Appalaches,plex,1.0,20,26.9,not_published
1472 +Ristigouche-Sud-Est,06035,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,20,23.9,not_published
1473 +Upton,48038,Montérégie,plex,1.0,20,25.4,not_published
1474 +Saint-Tite-des-Caps,21005,Capitale-Nationale,plex,1.0,20,25.4,not_published
1475 +Lac-aux-Sables,35010,Mauricie,plex,1.0,20,20.9,not_published
1476 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,plex,1.0,19,23.9,not_published
1477 +Saint-Placide,72043,Laurentides,plex,1.0,19,25.4,not_published
1478 +Mandeville,52095,Lanaudière,plex,1.0,19,22.4,not_published
1479 +Desbiens,93005,Saguenay–Lac-Saint-Jean,plex,1.0,19,23.9,not_published
1480 +Grenville-sur-la-Rouge,76052,Laurentides,plex,1.0,19,22.4,not_published
1481 +Rivière-Beaudette,71005,Montérégie,plex,1.0,19,22.4,not_published
1482 +Cacouna,12057,Bas-Saint-Laurent,condo,1.0,19,22.4,not_published
1483 +La Visitation-de-Yamaska,50085,Centre-du-Québec,unifamilial,1.0,19,26.9,not_published
1484 +Sainte-Catherine-de-Hatley,45060,Estrie,plex,1.0,19,25.4,not_published
1485 +Hope Town,05020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,19,26.9,not_published
1486 +Sainte-Catherine-de-la-Jacques-Cartier,22005,Capitale-Nationale,condo,1.0,19,23.9,not_published
1487 +Saint-Lambert,87120,Abitibi-Témiscamingue,unifamilial,1.0,19,23.9,not_published
1488 +Saint-Jacques-le-Majeur-de-Wolfestown,31025,Chaudière-Appalaches,unifamilial,1.0,19,22.4,not_published
1489 +Grosse-Île,01042,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,19,22.4,not_published
1490 +Saint-Pascal,14018,Bas-Saint-Laurent,plex,1.0,19,23.9,not_published
1491 +Rivière-aux-Outardes,96902,Côte-Nord,unifamilial,1.0,19,25.4,not_published
1492 +Saint-Siméon,05055,Gaspésie–Îles-de-la-Madeleine,plex,1.0,19,23.9,not_published
1493 +Saint-Côme–Linière,29057,Chaudière-Appalaches,plex,1.0,19,25.4,not_published
1494 +Petite-Rivière-Saint-François,16005,Capitale-Nationale,plex,1.0,19,22.4,not_published
1495 +Saint-Bruno-de-Guigues,85045,Abitibi-Témiscamingue,plex,1.0,19,26.9,not_published
1496 +Pointe-des-Cascades,71055,Montérégie,plex,1.0,19,25.4,not_published
1497 +Lorraine,73025,Laurentides,condo,1.0,18,22.4,not_published
1498 +Saint-Isidore,67040,Montérégie,plex,1.0,18,19.4,not_published
1499 +Saint-Edmond-les-Plaines,92050,Saguenay–Lac-Saint-Jean,unifamilial,1.0,18,22.4,not_published
1500 +Saint-Dominique,54060,Montérégie,plex,1.0,18,23.9,not_published
1501 +La Martre,04030,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,18,25.4,not_published
1502 +Saint-Gédéon-de-Beauce,29013,Chaudière-Appalaches,plex,1.0,18,22.4,not_published
1503 +Venise-en-Québec,56005,Montérégie,plex,1.0,18,26.9,not_published
1504 +Saint-Joseph-de-Kamouraska,14030,Bas-Saint-Laurent,unifamilial,1.0,18,25.4,not_published
1505 +Shawville,84010,Outaouais,plex,1.0,18,22.4,not_published
1506 +Pierreville,50113,Centre-du-Québec,plex,1.0,18,19.4,not_published
1507 +Dunham,46050,Estrie,plex,1.0,18,25.4,not_published
1508 +Saint-Gilbert,34060,Capitale-Nationale,unifamilial,1.0,18,22.4,not_published
1509 +Saint-Fabien,10070,Bas-Saint-Laurent,plex,1.0,18,22.4,not_published
1510 +Deux-Montagnes,72010,Laurentides,condo,1.0,18,20.9,not_published
1511 +Saint-Narcisse,37240,Mauricie,plex,1.0,18,23.9,not_published
1512 +Saint-Cléophas-de-Brandon,52075,Lanaudière,unifamilial,1.0,18,20.9,not_published
1513 +Sainte-Marcelline-de-Kildare,62030,Lanaudière,plex,1.0,18,23.9,not_published
1514 +Saint-Paul-d'Abbotsford,55015,Montérégie,plex,1.0,18,19.4,not_published
1515 +Mansfield-et-Pontefract,84065,Outaouais,plex,1.0,18,23.9,not_published
1516 +La Malbaie,15013,Capitale-Nationale,condo,1.0,18,22.4,not_published
1517 +Albertville,07025,Bas-Saint-Laurent,unifamilial,1.0,18,20.9,not_published
1518 +Pincourt,71070,Montérégie,plex,1.0,18,22.4,not_published
1519 +La Guadeloupe,29030,Chaudière-Appalaches,plex,1.0,18,19.4,not_published
1520 +Grande-Rivière,02015,Gaspésie–Îles-de-la-Madeleine,plex,1.0,18,22.4,not_published
1521 +Saint-Éphrem-de-Beauce,29112,Chaudière-Appalaches,plex,1.0,18,23.9,not_published
1522 +Hampden,41075,Estrie,unifamilial,1.0,18,23.9,not_published
1523 +Chambord,91020,Saguenay–Lac-Saint-Jean,plex,1.0,18,20.9,not_published
1524 +Saint-Pierre-de-l'Île-d'Orléans,20025,Capitale-Nationale,condo,1.0,18,22.4,not_published
1525 +Saint-Ignace-de-Loyola,52045,Lanaudière,plex,1.0,18,23.9,not_published
1526 +Saint-Siméon,15058,Capitale-Nationale,plex,1.0,18,23.9,not_published
1527 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,plex,1.0,18,20.9,not_published
1528 +Saint-Athanase,13100,Bas-Saint-Laurent,unifamilial,1.0,18,22.4,not_published
1529 +Saint-Elzéar-de-Témiscouata,13085,Bas-Saint-Laurent,unifamilial,1.0,17,20.9,not_published
1530 +Saint-Pacôme,14070,Bas-Saint-Laurent,plex,1.0,17,16.4,not_published
1531 +Sainte-Anne-de-Sorel,53065,Montérégie,plex,1.0,17,20.9,not_published
1532 +Paspébiac,05032,Gaspésie–Îles-de-la-Madeleine,plex,1.0,17,20.9,not_published
1533 +Saint-Médard,11025,Bas-Saint-Laurent,unifamilial,1.0,17,20.9,not_published
1534 +Larouche,94265,Saguenay–Lac-Saint-Jean,plex,1.0,17,22.4,not_published
1535 +Shefford,47035,Estrie,plex,1.0,17,20.9,not_published
1536 +Sacré-Coeur,95010,Côte-Nord,plex,1.0,17,19.4,not_published
1537 +Aguanish,98030,Côte-Nord,unifamilial,1.0,17,22.4,not_published
1538 +Val-des-Lacs,78100,Laurentides,plex,1.0,17,22.4,not_published
1539 +Brébeuf,78075,Laurentides,plex,1.0,17,20.9,not_published
1540 +Saint-Polycarpe,71020,Montérégie,plex,1.0,17,22.4,not_published
1541 +Saint-Pamphile,17010,Chaudière-Appalaches,plex,1.0,17,23.9,not_published
1542 +Champlain,37220,Mauricie,plex,1.0,17,20.9,not_published
1543 +Saint-Alphonse-de-Granby,47010,Estrie,plex,1.0,17,20.9,not_published
1544 +Les Escoumins,95025,Côte-Nord,plex,1.0,17,25.4,not_published
1545 +Saint-Philibert,29065,Chaudière-Appalaches,unifamilial,1.0,17,20.9,not_published
1546 +Saint-François-Xavier-de-Brompton,42020,Estrie,plex,1.0,17,20.9,not_published
1547 +Notre-Dame-de-l'Île-Perrot,71065,Montérégie,condo,1.0,17,19.4,not_published
1548 +Saint-Hyacinthe,54048,Montérégie,condo,1.0,17,20.9,not_published
1549 +Lorrainville,85037,Abitibi-Témiscamingue,plex,1.0,17,23.9,not_published
1550 +Sept-Îles,97007,Côte-Nord,condo,1.0,17,17.9,not_published
1551 +Montebello,80010,Outaouais,plex,1.0,17,17.9,not_published
1552 +Saint-Jean-de-Dieu,11010,Bas-Saint-Laurent,plex,1.0,17,22.4,not_published
1553 +Farnham,46112,Estrie,condo,1.0,17,23.9,not_published
1554 +Chertsey,62047,Lanaudière,plex,1.0,17,25.4,not_published
1555 +Saint-Liboire,54072,Montérégie,plex,1.0,16,19.4,not_published
1556 +Causapscal,07018,Bas-Saint-Laurent,plex,1.0,16,22.4,not_published
1557 +Belcourt,89050,Abitibi-Témiscamingue,unifamilial,1.0,16,19.4,not_published
1558 +Saint-Marc-sur-Richelieu,57050,Montérégie,plex,1.0,16,19.4,not_published
1559 +Saint-Cléophas,07090,Bas-Saint-Laurent,unifamilial,1.0,16,22.4,not_published
1560 +Rivière-à-Claude,04020,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,16,17.9,not_published
1561 +Saint-Gervais,19075,Chaudière-Appalaches,plex,1.0,16,19.4,not_published
1562 +Stanbridge Station,46030,Estrie,unifamilial,1.0,16,17.9,not_published
1563 +Sainte-Hélène-de-Bagot,54095,Montérégie,plex,1.0,16,19.4,not_published
1564 +L'Île-d'Anticosti,98020,Côte-Nord,unifamilial,1.0,16,20.9,not_published
1565 +Potton,45030,Estrie,plex,1.0,16,22.4,not_published
1566 +Saint-Gilles,33035,Chaudière-Appalaches,plex,1.0,16,19.4,not_published
1567 +Hudson,71100,Montérégie,plex,1.0,16,19.4,not_published
1568 +Natashquan,98025,Côte-Nord,unifamilial,1.0,16,19.4,not_published
1569 +Sainte-Émélie-de-l'Énergie,62070,Lanaudière,plex,1.0,16,23.9,not_published
1570 +Nominingue,79030,Laurentides,plex,1.0,16,20.9,not_published
1571 +Saint-Ulric,08073,Bas-Saint-Laurent,plex,1.0,16,22.4,not_published
1572 +Saint-Germain-de-Kamouraska,14045,Bas-Saint-Laurent,unifamilial,1.0,16,20.9,not_published
1573 +Saint-Polycarpe,71020,Montérégie,condo,1.0,16,17.9,not_published
1574 +Notre-Dame-du-Nord,85090,Abitibi-Témiscamingue,plex,1.0,16,20.9,not_published
1575 +Saint-Clet,71045,Montérégie,plex,1.0,16,22.4,not_published
1576 +Brownsburg-Chatham,76043,Laurentides,condo,1.0,16,19.4,not_published
1577 +Lac-Bouchette,91005,Saguenay–Lac-Saint-Jean,plex,1.0,15,17.9,not_published
1578 +Saint-Cuthbert,52062,Lanaudière,plex,1.0,15,19.4,not_published
1579 +Authier,87050,Abitibi-Témiscamingue,unifamilial,1.0,15,20.9,not_published
1580 +Saint-Étienne-des-Grès,51090,Mauricie,plex,1.0,15,20.9,not_published
1581 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,plex,1.0,15,19.4,not_published
1582 +Ripon,80078,Outaouais,plex,1.0,15,20.9,not_published
1583 +Saint-Zacharie,28005,Chaudière-Appalaches,plex,1.0,15,20.9,not_published
1584 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,plex,1.0,15,19.4,not_published
1585 +Saint-Denis-sur-Richelieu,57068,Montérégie,plex,1.0,15,17.9,not_published
1586 +Saint-Donat,62060,Lanaudière,condo,1.0,15,14.9,not_published
1587 +Léry,67055,Montérégie,plex,1.0,15,19.4,not_published
1588 +Lemieux,38020,Centre-du-Québec,unifamilial,1.0,15,19.4,not_published
1589 +Baie-D'Urfé,66112,Montréal,condo,1.0,15,20.9,not_published
1590 +Notre-Dame-des-Prairies,61030,Lanaudière,condo,1.0,15,17.9,not_published
1591 +Port-Daniel–Gascons,02047,Gaspésie–Îles-de-la-Madeleine,plex,1.0,15,20.9,not_published
1592 +Coteau-du-Lac,71040,Montérégie,condo,1.0,15,17.9,not_published
1593 +Franklin,69010,Montérégie,plex,1.0,15,20.9,not_published
1594 +Mont-Saint-Pierre,04015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,14,16.4,not_published
1595 +Shigawake,05010,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,14,17.9,not_published
1596 +Stanstead,45025,Estrie,plex,1.0,14,17.9,not_published
1597 +Sayabec,07085,Bas-Saint-Laurent,plex,1.0,14,19.4,not_published
1598 +Pont-Rouge,34017,Capitale-Nationale,condo,1.0,14,20.9,not_published
1599 +Maria,06005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,14,19.4,not_published
1600 +Napierville,68030,Montérégie,condo,1.0,14,16.4,not_published
1601 +Saint-Anicet,69070,Montérégie,plex,1.0,14,20.9,not_published
1602 +Nouvelle,06020,Gaspésie–Îles-de-la-Madeleine,plex,1.0,14,17.9,not_published
1603 +L'Ascension-de-Notre-Seigneur,93065,Saguenay–Lac-Saint-Jean,plex,1.0,14,17.9,not_published
1604 +Hampstead,66062,Montréal,condo,1.0,14,17.9,not_published
1605 +Compton,44071,Estrie,plex,1.0,14,19.4,not_published
1606 +Saint-Alexis,63023,Lanaudière,plex,1.0,14,14.9,not_published
1607 +Saint-Édouard-de-Lotbinière,33080,Chaudière-Appalaches,plex,1.0,14,20.9,not_published
1608 +Lac-Poulin,29095,Chaudière-Appalaches,unifamilial,1.0,14,20.9,not_published
1609 +Durham-Sud,49015,Centre-du-Québec,plex,1.0,14,19.4,not_published
1610 +Saint-Pierre,61020,Lanaudière,unifamilial,1.0,14,20.9,not_published
1611 +Chute-aux-Outardes,96035,Côte-Nord,plex,1.0,14,17.9,not_published
1612 +Saint-Isidore,26063,Chaudière-Appalaches,plex,1.0,14,17.9,not_published
1613 +Saint-Gabriel-de-Brandon,52085,Lanaudière,plex,1.0,14,20.9,not_published
1614 +Belleterre,85065,Abitibi-Témiscamingue,unifamilial,1.0,13,16.4,not_published
1615 +Sainte-Geneviève-de-Berthier,52040,Lanaudière,plex,1.0,13,17.9,not_published
1616 +Saint-Gilles,33035,Chaudière-Appalaches,condo,1.0,13,13.4,not_published
1617 +Lac-Mégantic,30030,Estrie,condo,1.0,13,17.9,not_published
1618 +Sainte-Béatrix,62020,Lanaudière,plex,1.0,13,19.4,not_published
1619 +Saint-Stanislas-de-Kostka,70040,Montérégie,plex,1.0,13,16.4,not_published
1620 +Saint-Urbain,16055,Capitale-Nationale,plex,1.0,13,16.4,not_published
1621 +Saint-Michel-de-Bellechasse,19110,Chaudière-Appalaches,plex,1.0,13,17.9,not_published
1622 +Saint-Liguori,63065,Lanaudière,plex,1.0,13,16.4,not_published
1623 +Saint-Honoré-de-Témiscouata,13090,Bas-Saint-Laurent,plex,1.0,13,17.9,not_published
1624 +Berthier-sur-Mer,18065,Chaudière-Appalaches,plex,1.0,13,17.9,not_published
1625 +Frelighsburg,46010,Estrie,plex,1.0,13,16.4,not_published
1626 +Sainte-Perpétue,17030,Chaudière-Appalaches,plex,1.0,13,17.9,not_published
1627 +La Morandière-Rochebaucourt,88012,Abitibi-Témiscamingue,unifamilial,1.0,13,16.4,not_published
1628 +Saint-Roch-de-l'Achigan,63035,Lanaudière,condo,1.0,13,17.9,not_published
1629 +Béarn,85020,Abitibi-Témiscamingue,plex,1.0,13,17.9,not_published
1630 +Fort-Coulonge,84060,Outaouais,plex,1.0,13,17.9,not_published
1631 +Saint-Louis-du-Ha! Ha!,13080,Bas-Saint-Laurent,plex,1.0,13,14.9,not_published
1632 +Sainte-Lucie-des-Laurentides,78020,Laurentides,plex,1.0,13,13.4,not_published
1633 +Vallée-Jonction,26015,Chaudière-Appalaches,plex,1.0,13,13.4,not_published
1634 +Entrelacs,62053,Lanaudière,plex,1.0,13,16.4,not_published
1635 +Armagh,19037,Chaudière-Appalaches,plex,1.0,13,19.4,not_published
1636 +Austin,45085,Estrie,plex,1.0,13,19.4,not_published
1637 +Mille-Isles,76030,Laurentides,plex,1.0,13,14.9,not_published
1638 +Messines,83060,Outaouais,plex,1.0,13,17.9,not_published
1639 +Launay,88080,Abitibi-Témiscamingue,unifamilial,1.0,13,17.9,not_published
1640 +Albanel,92030,Saguenay–Lac-Saint-Jean,plex,1.0,13,16.4,not_published
1641 +Rosemère,73020,Laurentides,condo,1.0,13,14.9,not_published
1642 +Tingwick,39025,Centre-du-Québec,plex,1.0,13,19.4,not_published
1643 +Lawrenceville,42045,Estrie,plex,1.0,13,16.4,not_published
1644 +Saint-Alexandre,56055,Montérégie,plex,1.0,13,17.9,not_published
1645 +Sheenboro,84095,Outaouais,unifamilial,1.0,13,16.4,not_published
1646 +Saint-Godefroi,05015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,13,16.4,not_published
1647 +L'Isle-aux-Coudres,16023,Capitale-Nationale,plex,1.0,12,16.4,not_published
1648 +Plaisance,80045,Outaouais,plex,1.0,12,16.4,not_published
1649 +Côte-Nord-du-Golfe-du-Saint-Laurent,98015,Côte-Nord,unifamilial,1.0,12,16.4,not_published
1650 +Lambton,30095,Estrie,plex,1.0,12,14.9,not_published
1651 +Lac-Simon,80095,Outaouais,plex,1.0,12,14.9,not_published
1652 +Saint-Antoine-de-Tilly,33095,Chaudière-Appalaches,plex,1.0,12,16.4,not_published
1653 +Saint-Damien-de-Buckland,19030,Chaudière-Appalaches,plex,1.0,12,16.4,not_published
1654 +Godmanchester,69060,Montérégie,plex,1.0,12,17.9,not_published
1655 +Saint-Étienne-de-Beauharnois,70030,Montérégie,plex,1.0,12,16.4,not_published
1656 +Adstock,31056,Chaudière-Appalaches,plex,1.0,12,17.9,not_published
1657 +Saint-Elphège,50095,Centre-du-Québec,unifamilial,1.0,12,16.4,not_published
1658 +Nicolet,50072,Centre-du-Québec,condo,1.0,12,17.9,not_published
1659 +Saint-Damase,54017,Montérégie,plex,1.0,12,14.9,not_published
1660 +Saint-Bernard,26055,Chaudière-Appalaches,plex,1.0,12,17.9,not_published
1661 +Wotton,40017,Estrie,plex,1.0,12,17.9,not_published
1662 +Sainte-Marguerite-Marie,07005,Bas-Saint-Laurent,unifamilial,1.0,12,14.9,not_published
1663 +Venise-en-Québec,56005,Montérégie,condo,1.5,12,11.9,not_published
1664 +Saint-Jean-de-Cherbourg,08010,Bas-Saint-Laurent,unifamilial,1.0,12,14.9,not_published
1665 +Saint-André-de-Restigouche,06040,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,12,14.9,not_published
1666 +Saint-Victor,27008,Chaudière-Appalaches,plex,1.0,12,16.4,not_published
1667 +Otter Lake,84055,Outaouais,plex,1.0,12,14.9,not_published
1668 +Baie-du-Febvre,50100,Centre-du-Québec,plex,1.0,11,13.4,not_published
1669 +Saint-Benoît-Labre,29100,Chaudière-Appalaches,plex,1.0,11,13.4,not_published
1670 +Saint-Joseph-de-Coleraine,31045,Chaudière-Appalaches,plex,1.0,11,14.9,not_published
1671 +Chelsea,82025,Outaouais,condo,1.0,11,11.9,not_published
1672 +Saint-Mathieu-de-Beloeil,57045,Montérégie,plex,1.0,11,16.4,not_published
1673 +Saint-Stanislas,37245,Mauricie,plex,1.0,11,14.9,not_published
1674 +La Minerve,78130,Laurentides,plex,1.0,11,16.4,not_published
1675 +Saint-Mathieu,67005,Montérégie,plex,1.0,11,16.4,not_published
1676 +Saint-Ours,53032,Montérégie,plex,1.0,11,16.4,not_published
1677 +Campbell's Bay,84030,Outaouais,plex,1.0,11,14.9,not_published
1678 +Lac-Walker,97904,Côte-Nord,unifamilial,1.0,11,14.9,not_published
1679 +Saint-Louis-de-Gonzague,70035,Montérégie,plex,1.0,11,16.4,not_published
1680 +Saint-Alban,34097,Capitale-Nationale,plex,1.0,11,14.9,not_published
1681 +Saint-Alexandre-des-Lacs,07065,Bas-Saint-Laurent,unifamilial,1.0,11,14.9,not_published
1682 +Percé,02005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,11,14.9,not_published
1683 +Saint-Ferdinand,32013,Centre-du-Québec,plex,1.0,11,16.4,not_published
1684 +L'Anse-Saint-Jean,94210,Saguenay–Lac-Saint-Jean,condo,1.0,11,13.4,not_published
1685 +Sainte-Anne-de-Bellevue,66117,Montréal,condo,1.0,11,13.4,not_published
1686 +Saint-Mathieu-de-Beloeil,57045,Montérégie,condo,1.0,11,11.9,not_published
1687 +Murdochville,03025,Gaspésie–Îles-de-la-Madeleine,plex,1.0,11,16.4,not_published
1688 +Déléage,83070,Outaouais,plex,1.0,11,14.9,not_published
1689 +Saint-Guillaume,49113,Centre-du-Québec,plex,1.0,11,16.4,not_published
1690 +Saint-Honoré-de-Shenley,29038,Chaudière-Appalaches,plex,1.0,11,13.4,not_published
1691 +Notre-Dame-de-Lorette,92060,Saguenay–Lac-Saint-Jean,unifamilial,1.0,11,14.9,not_published
1692 +Dudswell,41117,Estrie,plex,1.0,10,14.9,not_published
1693 +Rivière-Saint-Jean,98050,Côte-Nord,unifamilial,1.0,10,13.4,not_published
1694 +Saint-Adelphe,35015,Mauricie,plex,1.0,10,14.9,not_published
1695 +Saint-Sylvestre,33007,Chaudière-Appalaches,plex,1.0,10,14.9,not_published
1696 +Batiscan,37210,Mauricie,plex,1.0,10,14.9,not_published
1697 +Sainte-Brigide-d'Iberville,56105,Montérégie,plex,1.0,10,11.9,not_published
1698 +Rapides-des-Joachims,84100,Outaouais,unifamilial,1.0,10,13.4,not_published
1699 +Saint-Chrysostome,69017,Montérégie,plex,1.0,10,11.9,not_published
1700 +Ange-Gardien,55008,Montérégie,condo,1.0,10,14.9,not_published
1701 +Deschaillons-sur-Saint-Laurent,38070,Centre-du-Québec,plex,1.0,10,11.9,not_published
1702 +Palmarolle,87025,Abitibi-Témiscamingue,plex,1.0,10,13.4,not_published
1703 +Saint-Juste-du-Lac,13040,Bas-Saint-Laurent,unifamilial,1.0,10,11.9,not_published
1704 +Saint-Antoine-sur-Richelieu,57075,Montérégie,plex,1.0,10,13.4,not_published
1705 +Matagami,99015,Nord-du-Québec,plex,1.0,10,14.9,not_published
1706 +Lachute,76020,Laurentides,condo,1.0,10,13.4,not_published
1707 +Val-Joli,42095,Estrie,plex,1.0,10,14.9,not_published
1708 +Saint-Roch-Ouest,63040,Lanaudière,unifamilial,1.0,10,13.4,not_published
1709 +Saint-Charles-Garnier,09010,Bas-Saint-Laurent,unifamilial,1.0,10,11.9,not_published
1710 +Saint-Blaise-sur-Richelieu,56065,Montérégie,plex,1.0,10,14.9,not_published
1711 +Chapais,99020,Nord-du-Québec,plex,1.0,10,13.4,not_published
1712 +Grand-Remous,83095,Outaouais,plex,1.0,10,14.9,not_published
1713 +Saint-Damien,62075,Lanaudière,plex,1.0,10,13.4,not_published
1714 +Notre-Dame-des-Sept-Douleurs,12045,Bas-Saint-Laurent,unifamilial,1.0,10,13.4,not_published
1715 +Saint-Michel,68050,Montérégie,plex,1.0,10,14.9,not_published
1716 +Saint-Robert,53020,Montérégie,plex,1.0,10,11.9,not_published
1717 +Wentworth-Nord,77060,Laurentides,plex,1.0,10,13.4,not_published
1718 +Massueville,53010,Montérégie,plex,1.0,10,13.4,not_published
1719 +Rivière-Bleue,13025,Bas-Saint-Laurent,plex,1.0,10,13.4,not_published
1720 +Beaumont,19105,Chaudière-Appalaches,plex,1.0,10,14.9,not_published
1721 +Price,09065,Bas-Saint-Laurent,plex,1.0,10,13.4,not_published
1722 +Bégin,94250,Saguenay–Lac-Saint-Jean,plex,1.0,10,14.9,not_published
1723 +Frelighsburg,46010,Estrie,condo,1.0,10,14.9,not_published
1724 +Saint-Célestin,50030,Centre-du-Québec,plex,1.0,10,14.9,not_published
1725 +Saint-Jude,54110,Montérégie,plex,1.0,10,14.9,not_published
1726 +Taschereau,87042,Abitibi-Témiscamingue,plex,1.0,10,14.9,not_published
1727 +Notre-Dame-du-Portage,12080,Bas-Saint-Laurent,plex,1.0,10,13.4,not_published
1728 +Saint-Venant-de-Paquette,44005,Estrie,unifamilial,1.0,10,14.9,not_published
1729 +Montpellier,80090,Outaouais,plex,1.0,10,14.9,not_published
1730 +Sainte-Cécile-de-Milton,47055,Estrie,plex,1.0,10,11.9,not_published
1731 +Bristol,84005,Outaouais,plex,1.0,10,11.9,not_published
1732 +Laurierville,32072,Centre-du-Québec,plex,1.0,10,14.9,not_published
1733 +Lac-Sainte-Marie,83020,Outaouais,plex,1.0,9,10.4,not_published
1734 +Sainte-Clotilde,68020,Montérégie,plex,1.0,9,10.4,not_published
1735 +Stanbridge East,46045,Estrie,plex,1.0,9,11.9,not_published
1736 +Hemmingford,68010,Montérégie,plex,1.0,9,10.4,not_published
1737 +Saint-Magloire,28075,Chaudière-Appalaches,plex,1.0,9,13.4,not_published
1738 +Saint-Léonard-de-Portneuf,34115,Capitale-Nationale,plex,1.0,9,13.4,not_published
1739 +Les Éboulements,16048,Capitale-Nationale,plex,1.0,9,10.4,not_published
1740 +Petite-Vallée,03015,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,9,13.4,not_published
1741 +Saint-Boniface,51085,Mauricie,condo,1.0,9,13.4,not_published
1742 +Frontenac,30025,Estrie,plex,1.0,9,13.4,not_published
1743 +Montréal-Ouest,66047,Montréal,condo,1.0,9,11.9,not_published
1744 +Saint-Cyrille-de-Lessard,17045,Chaudière-Appalaches,plex,1.0,9,13.4,not_published
1745 +Kingsey Falls,39097,Centre-du-Québec,plex,1.0,9,11.9,not_published
1746 +Saint-Théodore-d'Acton,48045,Montérégie,plex,1.0,9,11.9,not_published
1747 +Lochaber-Partie-Ouest,80060,Outaouais,plex,1.0,9,11.9,not_published
1748 +L'Isle-Verte,12043,Bas-Saint-Laurent,plex,1.0,9,11.9,not_published
1749 +L'Isle-aux-Allumettes,84082,Outaouais,plex,1.0,9,11.9,not_published
1750 +Bouchette,83050,Outaouais,plex,1.0,9,11.9,not_published
1751 +Girardville,92055,Saguenay–Lac-Saint-Jean,plex,1.0,9,13.4,not_published
1752 +Kingsbury,42070,Estrie,unifamilial,1.0,9,13.4,not_published
1753 +Saint-Philémon,19005,Chaudière-Appalaches,plex,1.0,9,13.4,not_published
1754 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,plex,1.0,9,11.9,not_published
1755 +Saint-Aimé-du-Lac-des-Îles,79022,Laurentides,plex,1.0,9,11.9,not_published
1756 +Tring-Jonction,27060,Chaudière-Appalaches,plex,1.0,9,11.9,not_published
1757 +Thetford Mines,31084,Chaudière-Appalaches,condo,1.0,9,13.4,not_published
1758 +Auclair,13045,Bas-Saint-Laurent,unifamilial,1.0,9,11.9,not_published
1759 +Saint-Henri-de-Taillon,93070,Saguenay–Lac-Saint-Jean,plex,1.0,9,13.4,not_published
1760 +Saint-Charles-sur-Richelieu,57057,Montérégie,plex,1.0,9,11.9,not_published
1761 +Saint-Patrice-de-Sherrington,68025,Montérégie,plex,1.0,9,11.9,not_published
1762 +Chesterville,39030,Centre-du-Québec,plex,1.0,9,11.9,not_published
1763 +Sainte-Monique,93075,Saguenay–Lac-Saint-Jean,plex,1.0,9,13.4,not_published
1764 +Sainte-Anne-de-Sabrevois,56060,Montérégie,plex,1.0,9,13.4,not_published
1765 +Mont-Blanc,78047,Laurentides,condo,1.0,9,13.4,not_published
1766 +Saint-Hilarion,16050,Capitale-Nationale,plex,1.0,9,13.4,not_published
1767 +L'Île-Dorval,66092,Montréal,unifamilial,1.0,9,13.4,not_published
1768 +Val-Alain,33070,Chaudière-Appalaches,plex,1.0,9,11.9,not_published
1769 +Saint-Michel-des-Saints,62085,Lanaudière,plex,1.0,8,11.9,not_published
1770 +Saint-Maxime-du-Mont-Louis,04010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,11.9,not_published
1771 +Saint-Ludger-de-Milot,93080,Saguenay–Lac-Saint-Jean,plex,1.0,8,10.4,not_published
1772 +Saint-François-de-Sales,91015,Saguenay–Lac-Saint-Jean,plex,1.0,8,11.9,not_published
1773 +Saint-Aimé-des-Lacs,15030,Capitale-Nationale,plex,1.0,8,11.9,not_published
1774 +Sainte-Hénédine,26040,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1775 +Saint-Simon,54090,Montérégie,plex,1.0,8,11.9,not_published
1776 +Sainte-Marguerite-du-Lac-Masson,77012,Laurentides,condo,1.0,8,10.4,not_published
1777 +Notre-Dame-du-Bon-Conseil,49075,Centre-du-Québec,plex,1.0,8,10.4,not_published
1778 +Lefebvre,49020,Centre-du-Québec,plex,1.0,8,11.9,not_published
1779 +Sainte-Hélène-de-Kamouraska,14025,Bas-Saint-Laurent,plex,1.0,8,11.9,not_published
1780 +Saint-François-de-la-Rivière-du-Sud,18060,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1781 +Sainte-Angèle-de-Monnoir,55030,Montérégie,plex,1.0,8,11.9,not_published
1782 +Howick,69025,Montérégie,plex,1.0,8,10.4,not_published
1783 +La Patrie,41027,Estrie,plex,1.0,8,11.9,not_published
1784 +Henryville,56042,Montérégie,plex,1.0,8,11.9,not_published
1785 +Cap-Chat,04047,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,10.4,not_published
1786 +Sainte-Marie-Salomé,63005,Lanaudière,plex,1.0,8,11.9,not_published
1787 +Saint-Paul-de-l'Île-aux-Noix,56035,Montérégie,plex,1.0,8,10.4,not_published
1788 +Sainte-Anne-de-la-Rochelle,42050,Estrie,plex,1.0,8,10.4,not_published
1789 +Lac-Drolet,30080,Estrie,plex,1.0,8,11.9,not_published
1790 +Kazabazua,83015,Outaouais,plex,1.0,8,11.9,not_published
1791 +Disraeli,31020,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1792 +Huberdeau,78065,Laurentides,plex,1.0,8,11.9,not_published
1793 +Saint-Michel,68050,Montérégie,condo,1.0,8,11.9,not_published
1794 +Saint-Valérien-de-Milton,54065,Montérégie,plex,1.0,8,11.9,not_published
1795 +Saint-Séverin,35020,Mauricie,plex,1.0,8,11.9,not_published
1796 +Saint-Lucien,49030,Centre-du-Québec,plex,1.0,8,11.9,not_published
1797 +Saint-Lazare-de-Bellechasse,19050,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1798 +Mont-Carmel,14005,Bas-Saint-Laurent,plex,1.0,8,10.4,not_published
1799 +Pointe-à-la-Croix,06030,Gaspésie–Îles-de-la-Madeleine,plex,1.0,8,11.9,not_published
1800 +Gore,76025,Laurentides,plex,1.0,8,11.9,not_published
1801 +Saint-Flavien,33052,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1802 +Notre-Dame-de-Pontmain,79010,Laurentides,plex,1.0,8,10.4,not_published
1803 +Bury,41070,Estrie,plex,1.0,8,10.4,not_published
1804 +Sainte-Flavie,09085,Bas-Saint-Laurent,plex,1.0,8,10.4,not_published
1805 +Stratford,30110,Estrie,plex,1.0,8,10.4,not_published
1806 +Saint-Christophe-d'Arthabaska,39060,Centre-du-Québec,plex,1.0,8,11.9,not_published
1807 +Lotbinière,33115,Chaudière-Appalaches,plex,1.0,8,11.9,not_published
1808 +Saint-Paul-de-Montminy,18030,Chaudière-Appalaches,plex,1.0,7,10.4,not_published
1809 +Cap-Santé,34030,Capitale-Nationale,condo,1.0,7,10.4,not_published
1810 +La Durantaye,19090,Chaudière-Appalaches,plex,1.0,7,10.4,not_published
1811 +Saint-Ubalde,34090,Capitale-Nationale,plex,1.0,7,10.4,not_published
1812 +Saint-Fabien-de-Panet,18015,Chaudière-Appalaches,plex,1.0,7,10.4,not_published
1813 +Saint-Mathieu-du-Parc,51070,Mauricie,plex,1.0,7,10.4,not_published
1814 +Saint-Malachie,19025,Chaudière-Appalaches,plex,1.0,7,9.0,not_published
1815 +East Farnham,46085,Estrie,plex,1.0,7,9.0,not_published
1816 +Baie-Johan-Beetz,98035,Côte-Nord,unifamilial,1.0,7,9.0,not_published
1817 +Sainte-Félicité,08023,Bas-Saint-Laurent,plex,1.0,7,10.4,not_published
1818 +Saint-Édouard-de-Fabre,85015,Abitibi-Témiscamingue,plex,1.0,7,10.4,not_published
1819 +Bowman,80145,Outaouais,plex,1.0,7,9.0,not_published
1820 +Saint-Agapit,33045,Chaudière-Appalaches,condo,1.0,7,10.4,not_published
1821 +Sainte-Angèle-de-Mérici,09035,Bas-Saint-Laurent,plex,1.0,7,10.4,not_published
1822 +Saint-Modeste,12020,Bas-Saint-Laurent,plex,1.0,7,10.4,not_published
1823 +Lac-au-Saumon,07057,Bas-Saint-Laurent,plex,1.0,7,9.0,not_published
1824 +Baie-Comeau,96020,Côte-Nord,condo,1.0,7,10.4,not_published
1825 +Stoneham-et-Tewkesbury,22035,Capitale-Nationale,condo,1.0,7,9.0,not_published
1826 +Leclercville,33123,Chaudière-Appalaches,plex,1.0,7,10.4,not_published
1827 +Lac-des-Plages,80130,Outaouais,plex,1.0,7,10.4,not_published
1828 +Ham-Nord,39010,Centre-du-Québec,plex,1.0,7,9.0,not_published
1829 +Notre-Dame-de-Montauban,35005,Mauricie,plex,1.0,7,10.4,not_published
1830 +Fossambault-sur-le-Lac,22010,Capitale-Nationale,plex,1.0,7,7.5,not_published
1831 +Labrecque,93055,Saguenay–Lac-Saint-Jean,plex,1.0,7,10.4,not_published
1832 +Saint-Côme,62065,Lanaudière,condo,1.0,7,10.4,not_published
1833 +Saint-Irénée,15005,Capitale-Nationale,plex,1.0,7,10.4,not_published
1834 +New Carlisle,05040,Gaspésie–Îles-de-la-Madeleine,plex,1.0,7,9.0,not_published
1835 +Nantes,30045,Estrie,plex,1.0,7,10.4,not_published
1836 +Lorraine,73025,Laurentides,plex,1.0,7,10.4,not_published
1837 +Val-Morin,78005,Laurentides,condo,1.0,7,10.4,not_published
1838 +Saint-Félix-de-Kingsey,49005,Centre-du-Québec,plex,1.0,7,10.4,not_published
1839 +Piopolis,30020,Estrie,plex,1.0,7,9.0,not_published
1840 +Champneuf,88005,Abitibi-Témiscamingue,unifamilial,1.0,7,7.5,not_published
1841 +Laverlochère-Angliers,85052,Abitibi-Témiscamingue,plex,1.0,7,7.5,not_published
1842 +Chute-Saint-Philippe,79065,Laurentides,plex,1.0,7,9.0,not_published
1843 +Notre-Dame-de-la-Paix,80020,Outaouais,plex,1.5,7,6.0,not_published
1844 +Cayamant,83040,Outaouais,plex,1.0,7,9.0,not_published
1845 +Sainte-Ursule,51040,Mauricie,plex,1.0,7,10.4,not_published
1846 +North Hatley,45050,Estrie,plex,1.0,7,10.4,not_published
1847 +Sainte-Germaine-Boulé,87030,Abitibi-Témiscamingue,plex,1.0,7,10.4,not_published
1848 +Saint-Joseph-des-Érables,27050,Chaudière-Appalaches,unifamilial,1.0,7,9.0,not_published
1849 +Grande-Vallée,03020,Gaspésie–Îles-de-la-Madeleine,plex,1.0,7,10.4,not_published
1850 +Thurso,80050,Outaouais,condo,1.0,7,9.0,not_published
1851 +Val-des-Bois,80140,Outaouais,plex,1.0,7,10.4,not_published
1852 +Saint-Elzéar,26022,Chaudière-Appalaches,plex,1.0,7,10.4,not_published
1853 +Valcourt,42060,Estrie,plex,1.0,7,10.4,not_published
1854 +Sainte-Jeanne-d'Arc,92015,Saguenay–Lac-Saint-Jean,plex,1.0,7,10.4,not_published
1855 +Petit-Saguenay,94205,Saguenay–Lac-Saint-Jean,plex,1.0,6,7.5,not_published
1856 +Bonsecours,42040,Estrie,plex,1.0,6,9.0,not_published
1857 +Blanc-Sablon,98005,Côte-Nord,plex,1.0,6,9.0,not_published
1858 +Saint-Cyprien-de-Napierville,68035,Montérégie,plex,1.0,6,9.0,not_published
1859 +Hudson,71100,Montérégie,condo,1.0,6,7.5,not_published
1860 +L'Île-Cadieux,71095,Montérégie,unifamilial,1.0,6,9.0,not_published
1861 +Saint-Roch-des-Aulnaies,17065,Chaudière-Appalaches,plex,1.0,6,9.0,not_published
1862 +Rougemont,55037,Montérégie,condo,1.0,6,7.5,not_published
1863 +Saint-Gabriel-de-Rimouski,09025,Bas-Saint-Laurent,plex,1.0,6,9.0,not_published
1864 +Beaumont,19105,Chaudière-Appalaches,condo,1.0,6,9.0,not_published
1865 +Maddington Falls,39165,Centre-du-Québec,plex,1.0,6,9.0,not_published
1866 +Lantier,78015,Laurentides,plex,1.0,6,9.0,not_published
1867 +Hatley,45055,Estrie,plex,1.0,6,9.0,not_published
1868 +Saint-Félix-d'Otis,94225,Saguenay–Lac-Saint-Jean,plex,1.0,6,9.0,not_published
1869 +Bonne-Espérance,98010,Côte-Nord,unifamilial,1.0,6,7.5,not_published
1870 +Saint-Hilaire-de-Dorset,29020,Chaudière-Appalaches,unifamilial,1.0,6,9.0,not_published
1871 +Manseau,38028,Centre-du-Québec,plex,1.0,6,9.0,not_published
1872 +Donnacona,34025,Capitale-Nationale,condo,1.0,6,9.0,not_published
1873 +Notre-Dame-de-la-Merci,62055,Lanaudière,plex,1.0,6,9.0,not_published
1874 +Ayer's Cliff,45035,Estrie,plex,1.0,6,9.0,not_published
1875 +Saint-Hugues,54100,Montérégie,plex,1.0,6,6.0,not_published
1876 +Sainte-Marguerite,26035,Chaudière-Appalaches,plex,1.0,6,9.0,not_published
1877 +Namur,80110,Outaouais,plex,1.0,6,7.5,not_published
1878 +Saint-Majorique-de-Grantham,49095,Centre-du-Québec,plex,1.0,6,9.0,not_published
1879 +Natashquan,98025,Côte-Nord,plex,1.0,6,9.0,not_published
1880 +Laforce,85070,Abitibi-Témiscamingue,unifamilial,1.0,6,9.0,not_published
1881 +Saint-Élie-de-Caxton,51075,Mauricie,plex,1.0,6,7.5,not_published
1882 +Saint-René,29050,Chaudière-Appalaches,plex,1.0,6,9.0,not_published
1883 +Landrienne,88035,Abitibi-Témiscamingue,plex,1.0,6,9.0,not_published
1884 +Saint-Cyprien,28040,Chaudière-Appalaches,plex,1.0,6,9.0,not_published
1885 +Parisville,38055,Centre-du-Québec,plex,1.0,6,9.0,not_published
1886 +Vaudreuil-sur-le-Lac,71090,Montérégie,plex,1.0,6,9.0,not_published
1887 +Normétal,87115,Abitibi-Témiscamingue,plex,1.0,6,9.0,not_published
1888 +Hérouxville,35035,Mauricie,plex,1.0,6,9.0,not_published
1889 +Lac-du-Cerf,79015,Laurentides,plex,1.0,6,9.0,not_published
1890 +Clarenceville,56010,Montérégie,plex,1.0,6,9.0,not_published
1891 +Val-Saint-Gilles,87105,Abitibi-Témiscamingue,unifamilial,1.0,6,7.5,not_published
1892 +Saint-Clet,71045,Montérégie,condo,1.0,6,9.0,not_published
1893 +Verchères,59025,Montérégie,condo,1.0,6,9.0,not_published
1894 +Hinchinbrooke,69045,Montérégie,plex,1.0,6,9.0,not_published
1895 +Saint-Charles-Borromée,61035,Lanaudière,condo,1.0,6,7.5,not_published
1896 +Lac-Supérieur,78095,Laurentides,condo,1.0,6,9.0,not_published
1897 +Pointe-des-Cascades,71055,Montérégie,condo,1.0,6,9.0,not_published
1898 +Saint-Urbain-Premier,70005,Montérégie,plex,1.0,6,7.5,not_published
1899 +Lac-Chicobi,88904,Abitibi-Témiscamingue,unifamilial,2.0,6,4.5,not_published
1900 +Cleveland,42110,Estrie,plex,1.0,6,7.5,not_published
1901 +Honfleur,19070,Chaudière-Appalaches,plex,1.0,6,9.0,not_published
1902 +La Doré,91050,Saguenay–Lac-Saint-Jean,plex,1.0,6,7.5,not_published
1903 +Charette,51080,Mauricie,plex,1.0,6,9.0,not_published
1904 +Delson,67025,Montérégie,condo,1.0,6,7.5,not_published
1905 +Warden,47030,Estrie,plex,1.0,6,9.0,not_published
1906 +Sainte-Julienne,63060,Lanaudière,condo,1.0,6,9.0,not_published
1907 +Notre-Dame-de-la-Salette,80087,Outaouais,plex,1.0,6,7.5,not_published
1908 +Ayer's Cliff,45035,Estrie,condo,1.0,6,9.0,not_published
1909 +Sainte-Brigitte-de-Laval,22045,Capitale-Nationale,condo,1.0,6,9.0,not_published
1910 +Sainte-Pétronille,20030,Capitale-Nationale,plex,1.0,6,9.0,not_published
1911 +Saint-Armand,46017,Estrie,plex,1.0,6,7.5,not_published
1912 +Saint-Bernard-de-Lacolle,68005,Montérégie,plex,1.0,6,9.0,not_published
1913 +Arundel,78060,Laurentides,plex,1.0,6,7.5,not_published
1914 +Saint-Valérien,10060,Bas-Saint-Laurent,plex,1.0,6,9.0,not_published
1915 +Saint-Romain,30100,Estrie,plex,1.0,5,7.5,not_published
1916 +Roberval,91025,Saguenay–Lac-Saint-Jean,condo,1.0,5,6.0,not_published
1917 +Saint-Pierre-de-Broughton,31135,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1918 +Val-David,78010,Laurentides,condo,1.0,5,7.5,not_published
1919 +Saint-Sébastien,30085,Estrie,plex,1.0,5,7.5,not_published
1920 +Sainte-Clotilde-de-Beauce,31060,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1921 +Hemmingford,68015,Montérégie,condo,1.0,5,7.5,not_published
1922 +Estérel,77011,Laurentides,condo,1.0,5,6.0,not_published
1923 +Saint-Benjamin,28025,Chaudière-Appalaches,plex,1.0,5,6.0,not_published
1924 +Sainte-Eulalie,50005,Centre-du-Québec,plex,1.0,5,7.5,not_published
1925 +Saint-Mathieu-de-Rioux,11050,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1926 +L'Ascension-de-Patapédia,06060,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,5,7.5,not_published
1927 +La Pocatière,14082,Bas-Saint-Laurent,condo,1.0,5,7.5,not_published
1928 +Ragueneau,96040,Côte-Nord,plex,1.0,5,7.5,not_published
1929 +Sainte-Geneviève-de-Batiscan,37215,Mauricie,plex,1.0,5,7.5,not_published
1930 +Mont-Saint-Michel,79110,Laurentides,plex,1.0,5,7.5,not_published
1931 +Métis-sur-Mer,09048,Bas-Saint-Laurent,plex,1.0,5,6.0,not_published
1932 +Grandes-Piles,35040,Mauricie,plex,1.0,5,7.5,not_published
1933 +Sainte-Perpétue,50050,Centre-du-Québec,plex,1.0,5,7.5,not_published
1934 +Pohénégamook,13095,Bas-Saint-Laurent,condo,1.0,5,7.5,not_published
1935 +Saint-Sixte,80070,Outaouais,plex,1.0,5,7.5,not_published
1936 +Brigham,46090,Estrie,plex,1.0,5,7.5,not_published
1937 +Sainte-Rose-de-Watford,28030,Chaudière-Appalaches,plex,1.0,5,6.0,not_published
1938 +Stornoway,30105,Estrie,plex,1.0,5,7.5,not_published
1939 +Sainte-Irène,07040,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1940 +Saint-Alexis-de-Matapédia,06050,Gaspésie–Îles-de-la-Madeleine,plex,1.0,5,7.5,not_published
1941 +Saint-Hubert-de-Rivière-du-Loup,12010,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1942 +Saint-Claude,42100,Estrie,plex,1.0,5,7.5,not_published
1943 +Saint-Luc-de-Bellechasse,28060,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1944 +Sainte-Hedwidge,91030,Saguenay–Lac-Saint-Jean,plex,1.0,5,6.0,not_published
1945 +Notre-Dame-du-Bon-Conseil,49080,Centre-du-Québec,plex,1.0,5,7.5,not_published
1946 +Sainte-Angèle-de-Prémont,51055,Mauricie,plex,1.0,5,7.5,not_published
1947 +Les Bergeronnes,95018,Côte-Nord,plex,1.0,5,7.5,not_published
1948 +Waltham,84070,Outaouais,plex,1.0,5,7.5,not_published
1949 +Les Lacs-du-Témiscamingue,85907,Abitibi-Témiscamingue,unifamilial,1.0,5,7.5,not_published
1950 +Saint-Jacques-le-Mineur,68040,Montérégie,plex,1.0,5,7.5,not_published
1951 +Saint-Wenceslas,50023,Centre-du-Québec,plex,1.0,5,7.5,not_published
1952 +Saint-Césaire,55023,Montérégie,condo,1.0,5,6.0,not_published
1953 +Saint-Janvier-de-Joly,33065,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1954 +Saint-Émile-de-Suffolk,80125,Outaouais,plex,2.0,5,4.5,not_published
1955 +Sainte-Anne-du-Lac,79115,Laurentides,plex,1.0,5,6.0,not_published
1956 +La Corne,88030,Abitibi-Témiscamingue,plex,1.0,5,7.5,not_published
1957 +Notre-Dame-des-Monts,15025,Capitale-Nationale,plex,1.0,5,7.5,not_published
1958 +Dosquet,33040,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1959 +Inverness,32058,Centre-du-Québec,plex,1.0,5,6.0,not_published
1960 +Saint-Cyprien,12005,Bas-Saint-Laurent,plex,1.0,5,6.0,not_published
1961 +Duhamel-Ouest,85030,Abitibi-Témiscamingue,plex,1.0,5,7.5,not_published
1962 +Saint-André-de-Kamouraska,14040,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1963 +Saint-Simon-les-Mines,29125,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1964 +Montcalm,78055,Laurentides,plex,1.0,5,7.5,not_published
1965 +Scott,26048,Chaudière-Appalaches,plex,1.0,5,6.0,not_published
1966 +Matapédia,06045,Gaspésie–Îles-de-la-Madeleine,plex,1.0,5,7.5,not_published
1967 +Lac-Delage,22030,Capitale-Nationale,plex,1.0,5,7.5,not_published
1968 +Saint-Philippe-de-Néri,14060,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1969 +Val-Brillant,07080,Bas-Saint-Laurent,plex,1.0,5,7.5,not_published
1970 +Saint-Aubert,17055,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1971 +Fassett,80005,Outaouais,plex,1.0,5,6.0,not_published
1972 +Saint-Vallier,19117,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1973 +Sainte-Marguerite,26035,Chaudière-Appalaches,condo,1.0,5,6.0,not_published
1974 +Belleterre,85065,Abitibi-Témiscamingue,plex,1.0,5,6.0,not_published
1975 +Saint-Zénon,62080,Lanaudière,plex,1.0,5,7.5,not_published
1976 +Saint-Camille-de-Lellis,28070,Chaudière-Appalaches,plex,1.0,5,7.5,not_published
1977 +Racine,42032,Estrie,plex,1.0,5,7.5,not_published
1978 +Frampton,26005,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
1979 +Barnston-Ouest,44045,Estrie,plex,1.0,4,6.0,not_published
1980 +Sainte-Aurélie,28015,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
1981 +Notre-Dame-des-Neiges,11045,Bas-Saint-Laurent,plex,1.0,4,6.0,not_published
1982 +Noyan,56015,Montérégie,plex,1.0,4,6.0,not_published
1983 +Saint-Théophile,29005,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
1984 +Mont-Albert,04902,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,4,6.0,not_published
1985 +Courcelles–Saint-Évariste,29027,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
1986 +Saint-Mathieu-du-Parc,51070,Mauricie,condo,1.0,4,6.0,not_published
1987 +Notre-Dame-de-Bonsecours,80015,Outaouais,plex,1.0,4,6.0,not_published
1988 +Saint-Michel-du-Squatec,13065,Bas-Saint-Laurent,plex,1.0,4,6.0,not_published
1989 +Saint-Louis-de-Gonzague,70035,Montérégie,condo,1.0,4,6.0,not_published
1990 +Duparquet,87005,Abitibi-Témiscamingue,plex,1.0,4,6.0,not_published
1991 +Sainte-Apolline-de-Patton,18025,Chaudière-Appalaches,plex,1.0,4,4.5,not_published
1992 +Neuville,34007,Capitale-Nationale,condo,1.0,4,6.0,not_published
1993 +Saint-Sylvère,38005,Centre-du-Québec,plex,1.0,4,6.0,not_published
1994 +Péribonka,92010,Saguenay–Lac-Saint-Jean,plex,1.0,4,6.0,not_published
1995 +Saint-Sébastien,56050,Montérégie,plex,1.0,4,6.0,not_published
1996 +Blue Sea,83045,Outaouais,plex,1.0,4,6.0,not_published
1997 +Lac-Saint-Joseph,22015,Capitale-Nationale,plex,1.0,4,6.0,not_published
1998 +Richelieu,55057,Montérégie,condo,1.0,4,6.0,not_published
1999 +Saint-Patrice-de-Beaurivage,33025,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
2000 +Newport,41037,Estrie,plex,1.0,4,6.0,not_published
2001 +Bolton-Ouest,46065,Estrie,plex,1.0,4,6.0,not_published
2002 +L'Avenir,49025,Centre-du-Québec,plex,1.0,4,6.0,not_published
2003 +Saint-Ferdinand,32013,Centre-du-Québec,condo,1.0,4,6.0,not_published
2004 +La Macaza,79047,Laurentides,plex,1.0,4,6.0,not_published
2005 +Nédélec,85100,Abitibi-Témiscamingue,plex,1.0,4,4.5,not_published
2006 +Notre-Dame-de-Stanbridge,46100,Estrie,plex,1.0,4,6.0,not_published
2007 +Trois-Rives,35055,Mauricie,plex,1.0,4,6.0,not_published
2008 +Saint-Georges-de-Windsor,40032,Estrie,plex,1.0,4,6.0,not_published
2009 +Saint-Vianney,07075,Bas-Saint-Laurent,plex,1.0,4,6.0,not_published
2010 +Saint-Albert,39085,Centre-du-Québec,plex,1.0,4,6.0,not_published
2011 +Saint-François-d'Assise,06055,Gaspésie–Îles-de-la-Madeleine,plex,1.0,4,6.0,not_published
2012 +Escuminac,06025,Gaspésie–Îles-de-la-Madeleine,plex,1.0,4,6.0,not_published
2013 +Saint-Ludger,30072,Estrie,plex,1.0,4,4.5,not_published
2014 +La Visitation-de-l'Île-Dupas,52050,Lanaudière,plex,1.0,4,6.0,not_published
2015 +Saint-Léon-de-Standon,19020,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
2016 +Saint-Odilon-de-Cranbourne,27035,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
2017 +Saint-Nazaire-d'Acton,48050,Montérégie,plex,1.0,4,6.0,not_published
2018 +Saint-Adrien,40010,Estrie,plex,1.0,4,6.0,not_published
2019 +Shannon,22020,Capitale-Nationale,condo,1.0,4,6.0,not_published
2020 +Dixville,44023,Estrie,plex,1.0,4,6.0,not_published
2021 +Sainte-Claire,19055,Chaudière-Appalaches,condo,1.0,4,6.0,not_published
2022 +Saint-Lambert-de-Lauzon,26070,Chaudière-Appalaches,condo,1.0,4,6.0,not_published
2023 +Lac-Casault,07908,Bas-Saint-Laurent,unifamilial,1.0,4,4.5,not_published
2024 +Sainte-Marthe,71110,Montérégie,plex,1.0,4,6.0,not_published
2025 +Saint-Valère,39135,Centre-du-Québec,plex,1.0,4,6.0,not_published
2026 +Sainte-Louise,17060,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
2027 +Saint-David-de-Falardeau,94245,Saguenay–Lac-Saint-Jean,condo,1.0,4,6.0,not_published
2028 +Trécesson,88075,Abitibi-Témiscamingue,plex,1.0,4,6.0,not_published
2029 +Packington,13015,Bas-Saint-Laurent,plex,1.0,4,6.0,not_published
2030 +Saint-Isidore-de-Clifton,41012,Estrie,plex,1.0,4,6.0,not_published
2031 +Bolton-Est,45095,Estrie,plex,1.0,4,6.0,not_published
2032 +Sainte-Agathe-de-Lotbinière,33017,Chaudière-Appalaches,plex,1.0,4,6.0,not_published
2033 +Roxton,48015,Montérégie,plex,1.0,4,6.0,not_published
2034 +Hatley,45043,Estrie,plex,1.0,4,6.0,not_published
2035 +Alleyn-et-Cawood,84050,Outaouais,plex,1.0,4,6.0,not_published
2036 +Kirkland,66102,Montréal,plex,1.0,4,6.0,not_published
2037 +Waterloo,47025,Estrie,condo,1.0,4,6.0,not_published
2038 +Scotstown,41080,Estrie,plex,1.0,4,6.0,not_published
2039 +Coaticook,44037,Estrie,condo,1.0,4,6.0,not_published
2040 +Saint-Étienne-de-Bolton,45100,Estrie,plex,1.0,4,6.0,not_published
2041 +Saint-Narcisse-de-Rimouski,10015,Bas-Saint-Laurent,plex,1.0,4,6.0,not_published
2042 +Saint-Augustin,98012,Côte-Nord,unifamilial,1.0,4,6.0,not_published
2043 +Sainte-Cécile-de-Whitton,30050,Estrie,plex,1.0,4,6.0,not_published
2044 +Egan-Sud,83075,Outaouais,plex,1.0,4,6.0,not_published
2045 +Baie-des-Sables,08080,Bas-Saint-Laurent,plex,1.0,4,4.5,not_published
2046 +L'Épiphanie,60037,Lanaudière,condo,1.0,4,6.0,not_published
2047 +Tadoussac,95005,Côte-Nord,plex,1.0,4,4.5,not_published
2048 +Rivière-Bonaventure,05902,Gaspésie–Îles-de-la-Madeleine,unifamilial,1.0,4,6.0,not_published
2049 +Hemmingford,68015,Montérégie,plex,1.0,4,6.0,not_published
2050 +Saint-Pierre-les-Becquets,38065,Centre-du-Québec,plex,1.0,3,4.5,not_published
2051 +Fortierville,38047,Centre-du-Québec,plex,1.0,3,4.5,not_published
2052 +Godbout,96010,Côte-Nord,plex,1.0,3,4.5,not_published
2053 +Saint-Ignace-de-Stanbridge,46095,Estrie,plex,1.0,3,4.5,not_published
2054 +Saint-Frédéric,27065,Chaudière-Appalaches,plex,1.0,3,4.5,not_published
2055 +Sainte-Clotilde,68020,Montérégie,condo,1.0,3,4.5,not_published
2056 +Saint-Philémon,19005,Chaudière-Appalaches,condo,1.0,3,4.5,not_published
2057 +L'Île-du-Grand-Calumet,84035,Outaouais,plex,1.0,3,4.5,not_published
2058 +Grand-Saint-Esprit,50065,Centre-du-Québec,plex,1.0,3,4.5,not_published
2059 +Sainte-Christine-d'Auvergne,34105,Capitale-Nationale,plex,1.0,3,4.5,not_published
2060 +Saint-Justin,51045,Mauricie,plex,1.0,3,4.5,not_published
2061 +Eeyou Istchee Baie-James,99060,Nord-du-Québec,plex,1.0,3,4.5,not_published
2062 +Saint-Épiphane,12030,Bas-Saint-Laurent,plex,1.5,3,3.0,not_published
2063 +East Hereford,44010,Estrie,plex,1.0,3,4.5,not_published
2064 +Saint-Elzéar,26022,Chaudière-Appalaches,condo,1.0,3,4.5,not_published
2065 +Plessisville,32043,Centre-du-Québec,condo,3.0,3,1.5,not_published
2066 +Moffet,85075,Abitibi-Témiscamingue,plex,1.0,3,4.5,not_published
2067 +Thorne,84045,Outaouais,plex,1.0,3,4.5,not_published
2068 +Dupuy,87085,Abitibi-Témiscamingue,plex,1.0,3,4.5,not_published
2069 +Lac-Simon,80095,Outaouais,condo,1.0,3,4.5,not_published
2070 +Sainte-Rita,11015,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2071 +Saint-Samuel,39130,Centre-du-Québec,plex,1.0,3,4.5,not_published
2072 +Westbury,41065,Estrie,plex,1.0,3,4.5,not_published
2073 +Saint-Augustin-de-Woburn,30005,Estrie,plex,1.0,3,4.5,not_published
2074 +Windsor,42088,Estrie,condo,1.0,3,4.5,not_published
2075 +Rivière-Ojima,87904,Abitibi-Témiscamingue,unifamilial,1.0,3,4.5,not_published
2076 +Saint-Édouard,68045,Montérégie,plex,1.0,3,4.5,not_published
2077 +Dundee,69075,Montérégie,plex,1.0,3,4.5,not_published
2078 +Aumond,83090,Outaouais,plex,1.0,3,4.5,not_published
2079 +Brome,46070,Estrie,plex,1.0,3,4.5,not_published
2080 +Sainte-Florence,07010,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2081 +Longue-Rive,95032,Côte-Nord,plex,1.0,3,4.5,not_published
2082 +Sagard,15904,Capitale-Nationale,unifamilial,1.5,3,3.0,not_published
2083 +Boileau,80115,Outaouais,plex,1.0,3,4.5,not_published
2084 +Très-Saint-Sacrement,69030,Montérégie,plex,1.0,3,4.5,not_published
2085 +Saint-Pierre,61020,Lanaudière,plex,1.0,3,4.5,not_published
2086 +Saint-Clément,11005,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2087 +Saint-Eugène-de-Guigues,85085,Abitibi-Témiscamingue,plex,1.0,3,4.5,not_published
2088 +Montmagny,18050,Chaudière-Appalaches,condo,1.0,3,4.5,not_published
2089 +Saint-Just-de-Bretenières,18005,Chaudière-Appalaches,plex,1.0,3,4.5,not_published
2090 +Montcerf-Lytton,83088,Outaouais,plex,1.0,3,4.5,not_published
2091 +Sainte-Hélène-de-Bagot,54095,Montérégie,condo,1.0,3,4.5,not_published
2092 +Sainte-Sophie-de-Lévrard,38040,Centre-du-Québec,plex,1.0,3,4.5,not_published
2093 +Saint-Gabriel,52080,Lanaudière,condo,1.0,3,4.5,not_published
2094 +Lac-Sergent,34120,Capitale-Nationale,plex,1.5,3,3.0,not_published
2095 +Saint-Luc-de-Vincennes,37225,Mauricie,plex,1.0,3,4.5,not_published
2096 +Saint-Bonaventure,49125,Centre-du-Québec,plex,1.0,3,4.5,not_published
2097 +Sainte-Thérèse-de-Gaspé,02010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,3,4.5,not_published
2098 +Rivière-Héva,89010,Abitibi-Témiscamingue,plex,1.0,3,4.5,not_published
2099 +Schefferville,97040,Côte-Nord,unifamilial,1.0,3,4.5,not_published
2100 +Saint-Marc-de-Figuery,88040,Abitibi-Témiscamingue,plex,1.5,3,3.0,not_published
2101 +Pointe-Lebel,96025,Côte-Nord,plex,1.0,3,4.5,not_published
2102 +Saint-Pierre-de-la-Rivière-du-Sud,18055,Chaudière-Appalaches,plex,1.0,3,4.5,not_published
2103 +Ivry-sur-le-Lac,78042,Laurentides,plex,1.0,3,4.5,not_published
2104 +Château-Richer,21035,Capitale-Nationale,condo,1.0,3,4.5,not_published
2105 +Saint-Nérée-de-Bellechasse,19045,Chaudière-Appalaches,plex,1.0,3,4.5,not_published
2106 +Saint-Eugène,49105,Centre-du-Québec,plex,1.0,3,4.5,not_published
2107 +Senneterre,89045,Abitibi-Témiscamingue,plex,1.0,3,4.5,not_published
2108 +Saint-Jacques-de-Leeds,31140,Chaudière-Appalaches,plex,1.0,3,4.5,not_published
2109 +Melbourne,42075,Estrie,plex,1.0,3,4.5,not_published
2110 +Saint-Gédéon,93035,Saguenay–Lac-Saint-Jean,condo,1.0,3,4.5,not_published
2111 +Matane,08053,Bas-Saint-Laurent,condo,1.0,3,4.5,not_published
2112 +Gros-Mécatina,98014,Côte-Nord,unifamilial,1.0,3,4.5,not_published
2113 +L'Islet,17078,Chaudière-Appalaches,condo,1.0,3,4.5,not_published
2114 +Saint-Prosper-de-Champlain,37250,Mauricie,plex,1.0,3,4.5,not_published
2115 +Stanstead-Est,44050,Estrie,plex,1.0,3,4.5,not_published
2116 +Saint-Eugène-de-Ladrière,10075,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2117 +Saint-François-Xavier-de-Viger,12025,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2118 +Bryson,84025,Outaouais,plex,1.0,3,4.5,not_published
2119 +Notre-Dame-des-Pins,29120,Chaudière-Appalaches,condo,1.0,3,4.5,not_published
2120 +Bois-Franc,83085,Outaouais,plex,1.5,3,3.0,not_published
2121 +Sainte-Sabine,46105,Estrie,plex,1.0,3,4.5,not_published
2122 +Esprit-Saint,10005,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2123 +Saint-Donat,09030,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2124 +Sainte-Rose-du-Nord,94230,Saguenay–Lac-Saint-Jean,plex,1.0,3,4.5,not_published
2125 +Saint-Barnabé-Sud,54105,Montérégie,plex,1.0,3,4.5,not_published
2126 +Notre-Dame-de-Lourdes,32080,Centre-du-Québec,plex,1.0,3,4.5,not_published
2127 +Saint-Augustin,92005,Saguenay–Lac-Saint-Jean,plex,1.0,3,4.5,not_published
2128 +Saint-Jean-de-l'Île-d'Orléans,20015,Capitale-Nationale,plex,1.0,3,4.5,not_published
2129 +Rivière-Ouelle,14065,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2130 +Lac-Saguay,79060,Laurentides,plex,1.0,3,4.5,not_published
2131 +Saint-Robert-Bellarmin,30070,Estrie,plex,1.0,3,4.5,not_published
2132 +Lejeune,13050,Bas-Saint-Laurent,unifamilial,1.0,3,4.5,not_published
2133 +Saint-Norbert-d'Arthabaska,39043,Centre-du-Québec,plex,1.0,3,4.5,not_published
2134 +Saint-Marc-du-Lac-Long,13020,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2135 +Saint-Léon-le-Grand,07030,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2136 +Litchfield,84040,Outaouais,plex,1.0,3,4.5,not_published
2137 +Saint-Moïse,07095,Bas-Saint-Laurent,plex,1.0,3,4.5,not_published
2138 +Audet,30055,Estrie,plex,1.0,2,3.0,not_published
2139 +Saint-François-de-l'Île-d'Orléans,20005,Capitale-Nationale,plex,1.0,2,3.0,not_published
2140 +Sainte-Victoire-de-Sorel,53025,Montérégie,condo,1.0,2,3.0,not_published
2141 +Kingsbury,42070,Estrie,plex,1.0,2,3.0,not_published
2142 +Saint-Joseph-de-Lepage,09070,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2143 +Wentworth,76035,Laurentides,plex,1.0,2,3.0,not_published
2144 +Lochaber,80055,Outaouais,plex,1.0,2,3.0,not_published
2145 +Saint-Joachim-de-Shefford,47040,Estrie,plex,1.0,2,3.0,not_published
2146 +Saint-Tharcisius,07070,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2147 +Saint-Stanislas-de-Kostka,70040,Montérégie,condo,1.0,2,3.0,not_published
2148 +Saint-Barnabé,51025,Mauricie,plex,1.0,2,3.0,not_published
2149 +Latulipe-et-Gaboury,85060,Abitibi-Témiscamingue,plex,1.0,2,3.0,not_published
2150 +Chichester,84090,Outaouais,plex,1.0,2,3.0,not_published
2151 +Rapides-des-Joachims,84100,Outaouais,plex,1.0,2,3.0,not_published
2152 +Saint-Edmond-de-Grantham,49100,Centre-du-Québec,plex,1.0,2,3.0,not_published
2153 +Sainte-Brigitte-des-Saults,49085,Centre-du-Québec,plex,1.0,2,3.0,not_published
2154 +Kiamika,79025,Laurentides,plex,2.0,2,1.5,not_published
2155 +Sainte-Paule,08040,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2156 +Sainte-Gertrude-Manneville,88085,Abitibi-Témiscamingue,plex,1.0,2,3.0,not_published
2157 +Saint-Mathias-sur-Richelieu,55065,Montérégie,condo,1.0,2,3.0,not_published
2158 +Sainte-Sophie-d'Halifax,32023,Centre-du-Québec,plex,1.0,2,3.0,not_published
2159 +Saint-Thomas-Didyme,92045,Saguenay–Lac-Saint-Jean,plex,1.0,2,3.0,not_published
2160 +Sainte-Madeleine-de-la-Rivière-Madeleine,04005,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,3.0,not_published
2161 +Poularies,87035,Abitibi-Témiscamingue,plex,1.0,2,3.0,not_published
2162 +Rivière-à-Pierre,34135,Capitale-Nationale,plex,1.0,2,3.0,not_published
2163 +Franquelin,96015,Côte-Nord,plex,1.0,2,3.0,not_published
2164 +Saint-Norbert,52070,Lanaudière,plex,1.0,2,3.0,not_published
2165 +Saint-Zéphirin-de-Courval,50090,Centre-du-Québec,plex,1.0,2,3.0,not_published
2166 +Larouche,94265,Saguenay–Lac-Saint-Jean,condo,1.0,2,3.0,not_published
2167 +Saint-Charles-de-Bourget,94260,Saguenay–Lac-Saint-Jean,plex,1.0,2,3.0,not_published
2168 +Saint-Camille,40025,Estrie,plex,1.0,2,3.0,not_published
2169 +Shigawake,05010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,3.0,not_published
2170 +Stanbridge Station,46030,Estrie,plex,2.0,2,1.5,not_published
2171 +Saint-Fortunat,31030,Chaudière-Appalaches,plex,1.0,2,3.0,not_published
2172 +Notre-Dame-des-Bois,30010,Estrie,plex,1.0,2,3.0,not_published
2173 +Senneville,66127,Montréal,plex,1.0,2,3.0,not_published
2174 +Biencourt,13055,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2175 +Longue-Pointe-de-Mingan,98045,Côte-Nord,plex,1.0,2,3.0,not_published
2176 +Havelock,69005,Montérégie,plex,1.0,2,3.0,not_published
2177 +Notre-Dame-des-Sept-Douleurs,12045,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2178 +Oka,72032,Laurentides,condo,1.0,2,3.0,not_published
2179 +Mulgrave-et-Derry,80085,Outaouais,plex,1.0,2,3.0,not_published
2180 +Scott,26048,Chaudière-Appalaches,condo,1.0,2,3.0,not_published
2181 +Huntingdon,69055,Montérégie,condo,1.0,2,3.0,not_published
2182 +L'Île-d'Anticosti,98020,Côte-Nord,plex,1.0,2,3.0,not_published
2183 +Lac-Huron,10902,Bas-Saint-Laurent,unifamilial,1.0,2,3.0,not_published
2184 +Saint-Elzéar,05050,Gaspésie–Îles-de-la-Madeleine,plex,1.0,2,3.0,not_published
2185 +Saint-Antoine-de-l'Isle-aux-Grues,18070,Chaudière-Appalaches,plex,1.0,2,3.0,not_published
2186 +Les Méchins,08005,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2187 +Crabtree,61013,Lanaudière,condo,1.0,2,3.0,not_published
2188 +Saints-Anges,26010,Chaudière-Appalaches,plex,1.0,2,3.0,not_published
2189 +Sainte-Marie-de-Blandford,38015,Centre-du-Québec,plex,1.0,2,3.0,not_published
2190 +Saint-Elphège,50095,Centre-du-Québec,plex,1.0,2,3.0,not_published
2191 +Saint-Pierre-Baptiste,32050,Centre-du-Québec,plex,1.0,2,3.0,not_published
2192 +Harrington,76065,Laurentides,plex,2.0,2,1.5,not_published
2193 +Fugèreville,85055,Abitibi-Témiscamingue,plex,1.0,2,3.0,not_published
2194 +Saint-Bernard,26055,Chaudière-Appalaches,condo,1.0,2,3.0,not_published
2195 +Saint-Didace,52090,Lanaudière,plex,1.0,2,3.0,not_published
2196 +Lamarche,93060,Saguenay–Lac-Saint-Jean,plex,1.0,2,3.0,not_published
2197 +Saint-André-du-Lac-Saint-Jean,91010,Saguenay–Lac-Saint-Jean,plex,1.0,2,3.0,not_published
2198 +Sainte-Séraphine,39105,Centre-du-Québec,plex,1.0,2,3.0,not_published
2199 +Saint-Roch-de-Mékinac,35045,Mauricie,plex,1.0,2,3.0,not_published
2200 +Saint-Zénon-du-Lac-Humqui,07035,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2201 +Rémigny,85105,Abitibi-Témiscamingue,plex,1.0,2,3.0,not_published
2202 +TNO aquatique de la MRC géographique de Montréal,66990,Montréal,unifamilial,1.0,2,3.0,not_published
2203 +Saint-Malachie,19025,Chaudière-Appalaches,condo,1.0,2,3.0,not_published
2204 +Villeroy,32085,Centre-du-Québec,plex,1.0,2,3.0,not_published
2205 +Elgin,69050,Montérégie,plex,1.0,2,3.0,not_published
2206 +Sainte-Anne-de-Beaupré,21030,Capitale-Nationale,condo,1.0,2,3.0,not_published
2207 +Saint-René-de-Matane,08035,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2208 +Baie-D'Urfé,66112,Montréal,plex,1.0,2,3.0,not_published
2209 +Chartierville,41020,Estrie,plex,1.0,2,3.0,not_published
2210 +Lac-Masketsi,35902,Mauricie,unifamilial,1.0,2,3.0,not_published
2211 +Sainte-Lucie-de-Beauregard,18020,Chaudière-Appalaches,plex,1.0,2,3.0,not_published
2212 +Pointe-Fortune,71140,Montérégie,plex,1.0,2,3.0,not_published
2213 +Pike River,46025,Estrie,plex,1.0,2,3.0,not_published
2214 +Saint-Thomas,61027,Lanaudière,condo,1.0,2,3.0,not_published
2215 +Low,83010,Outaouais,plex,1.0,2,3.0,not_published
2216 +Saint-Michel-des-Saints,62085,Lanaudière,condo,1.0,2,3.0,not_published
2217 +Notre-Dame-Auxiliatrice-de-Buckland,19010,Chaudière-Appalaches,plex,1.0,2,3.0,not_published
2218 +Colombier,95050,Côte-Nord,plex,1.0,2,3.0,not_published
2219 +Témiscouata-sur-le-Lac,13073,Bas-Saint-Laurent,condo,1.0,2,3.0,not_published
2220 +Sainte-Justine-de-Newton,71115,Montérégie,plex,1.0,2,3.0,not_published
2221 +Saint-Éloi,11035,Bas-Saint-Laurent,plex,1.0,2,3.0,not_published
2222 +Saint-Mathieu-d'Harricana,88050,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2223 +Aston-Jonction,50013,Centre-du-Québec,plex,1.0,1,1.5,not_published
2224 +Sainte-Monique,50057,Centre-du-Québec,plex,1.0,1,1.5,not_published
2225 +Sainte-Clotilde-de-Horton,39117,Centre-du-Québec,plex,1.0,1,1.5,not_published
2226 +Abercorn,46005,Estrie,plex,1.0,1,1.5,not_published
2227 +Portage-du-Fort,84020,Outaouais,plex,1.0,1,1.5,not_published
2228 +Portneuf-sur-Mer,95040,Côte-Nord,plex,1.0,1,1.5,not_published
2229 +Saint-Octave-de-Métis,09055,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2230 +Saint-Gabriel-Lalemant,14075,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2231 +Saint-Célestin,50035,Centre-du-Québec,plex,1.0,1,1.5,not_published
2232 +Saint-Omer,17005,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2233 +Saint-Stanislas,92070,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2234 +Saint-Dominique-du-Rosaire,88065,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2235 +Saint-Paul-de-la-Croix,12035,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2236 +Beaulac-Garthby,31008,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2237 +Saint-Anselme,19062,Chaudière-Appalaches,condo,1.0,1,1.5,not_published
2238 +Saint-Séverin,27070,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2239 +La Motte,88045,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2240 +Val-Racine,30015,Estrie,plex,1.0,1,1.5,not_published
2241 +Saint-Godefroi,05015,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2242 +La Rédemption,09005,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2243 +TNO aquatique de la MRC du Rocher-Percé,02990,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2244 +Sainte-Edwidge-de-Clifton,44055,Estrie,plex,1.0,1,1.5,not_published
2245 +Saint-Martin,29045,Chaudière-Appalaches,condo,1.0,1,1.5,not_published
2246 +Maricourt,42065,Estrie,plex,1.0,1,1.5,not_published
2247 +Saint-Laurent-de-l'Île-d'Orléans,20020,Capitale-Nationale,condo,1.0,1,1.5,not_published
2248 +Routhierville,07902,Bas-Saint-Laurent,unifamilial,1.0,1,1.5,not_published
2249 +Laniel,85905,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2250 +Saint-Guillaume-Nord,62912,Lanaudière,unifamilial,1.0,1,1.5,not_published
2251 +Sainte-Élizabeth-de-Warwick,39090,Centre-du-Québec,plex,1.0,1,1.5,not_published
2252 +Saint-Léonard-d'Aston,50042,Centre-du-Québec,condo,1.0,1,1.5,not_published
2253 +Saint-Gilbert,34060,Capitale-Nationale,plex,1.0,1,1.5,not_published
2254 +Auclair,13045,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2255 +Martinville,44060,Estrie,plex,1.0,1,1.5,not_published
2256 +Marston,30035,Estrie,plex,1.0,1,1.5,not_published
2257 +Pointe-aux-Outardes,96030,Côte-Nord,plex,1.0,1,1.5,not_published
2258 +Saint-Julien,31035,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2259 +Ulverton,42078,Estrie,plex,1.0,1,1.5,not_published
2260 +Nominingue,79030,Laurentides,condo,1.0,1,1.5,not_published
2261 +Côte-Nord-du-Golfe-du-Saint-Laurent,98015,Côte-Nord,plex,1.0,1,1.5,not_published
2262 +Cloridorme,03010,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2263 +Saint-Narcisse-de-Beaurivage,33030,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2264 +Belle-Rivière,93908,Saguenay–Lac-Saint-Jean,unifamilial,1.0,1,1.5,not_published
2265 +Clermont,87110,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2266 +Saint-Joseph-de-Kamouraska,14030,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2267 +Bedford,46040,Estrie,plex,1.0,1,1.5,not_published
2268 +Saint-Bernard-de-Michaudville,54115,Montérégie,plex,1.0,1,1.5,not_published
2269 +Saint-Rosaire,39145,Centre-du-Québec,plex,1.0,1,1.5,not_published
2270 +Sainte-Thècle,35050,Mauricie,condo,1.0,1,1.5,not_published
2271 +Kinnear's Mills,31105,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2272 +Sainte-Félicité,17025,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2273 +Saint-Alexis,63023,Lanaudière,condo,1.0,1,1.5,not_published
2274 +La Trinité-des-Monts,10010,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2275 +Saint-Télesphore,71015,Montérégie,plex,1.0,1,1.5,not_published
2276 +Clarendon,84015,Outaouais,plex,1.0,1,1.5,not_published
2277 +Grosse-Île,01042,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2278 +Saint-Adalbert,17015,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2279 +Sainte-Marguerite-Marie,07005,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2280 +Saint-Marcel-de-Richelieu,54125,Montérégie,plex,1.0,1,1.5,not_published
2281 +Saint-Eusèbe,13030,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2282 +Saint-Basile,34038,Capitale-Nationale,condo,1.0,1,1.5,not_published
2283 +Saint-Louis,54120,Montérégie,plex,1.0,1,1.5,not_published
2284 +Kipawa,85010,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2285 +Léry,67055,Montérégie,condo,1.0,1,1.5,not_published
2286 +Ogden,45020,Estrie,plex,1.0,1,1.5,not_published
2287 +Notre-Dame-de-Lorette,92060,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2288 +Sainte-Élisabeth-de-Proulx,92903,Saguenay–Lac-Saint-Jean,unifamilial,1.0,1,1.5,not_published
2289 +Sainte-Famille-de-l'Île-d'Orléans,20010,Capitale-Nationale,plex,1.0,1,1.5,not_published
2290 +TNO aquatique de la MRC de Pontiac,84990,Outaouais,plex,1.0,1,1.5,not_published
2291 +Saint-Louis-de-Blandford,39170,Centre-du-Québec,plex,1.0,1,1.5,not_published
2292 +Denholm,83005,Outaouais,plex,1.0,1,1.5,not_published
2293 +Saint-Herménégilde,44015,Estrie,plex,1.0,1,1.5,not_published
2294 +Saint-Venant-de-Paquette,44005,Estrie,plex,1.0,1,1.5,not_published
2295 +L'Ascension,79050,Laurentides,plex,1.0,1,1.5,not_published
2296 +Sainte-Élisabeth-de-Proulx,92903,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2297 +Mont-Élie,15902,Capitale-Nationale,plex,1.0,1,1.5,not_published
2298 +Saint-Jules,27055,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2299 +Estérel,77011,Laurentides,plex,1.0,1,1.5,not_published
2300 +Ferland-et-Boilleau,94220,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2301 +Sainte-Françoise,38035,Centre-du-Québec,plex,1.0,1,1.5,not_published
2302 +Saint-Célestin,50030,Centre-du-Québec,condo,1.0,1,1.5,not_published
2303 +Sainte-Christine,48020,Montérégie,plex,1.0,1,1.5,not_published
2304 +Saint-Léon-le-Grand,51035,Mauricie,plex,1.0,1,1.5,not_published
2305 +Saint-Alphonse,05065,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2306 +Mayo,80065,Outaouais,plex,1.0,1,1.5,not_published
2307 +Saint-André-de-Restigouche,06040,Gaspésie–Îles-de-la-Madeleine,plex,1.0,1,1.5,not_published
2308 +Milan,30040,Estrie,plex,1.0,1,1.5,not_published
2309 +Linton,34904,Capitale-Nationale,unifamilial,1.0,1,1.5,not_published
2310 +Amos,88057,Abitibi-Témiscamingue,condo,1.0,1,1.5,not_published
2311 +Carleton-sur-Mer,06013,Gaspésie–Îles-de-la-Madeleine,condo,1.0,1,1.5,not_published
2312 +Guérin,85095,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2313 +Hampden,41075,Estrie,plex,1.0,1,1.5,not_published
2314 +Berry,88070,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2315 +Grosses-Roches,08015,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2316 +Preissac,88090,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2317 +Saint-Adolphe-d'Howard,77065,Laurentides,condo,1.0,1,1.5,not_published
2318 +Saint-Eugène-d'Argentenay,92065,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2319 +Sainte-Thérèse-de-la-Gatineau,83055,Outaouais,plex,1.0,1,1.5,not_published
2320 +Calixa-Lavallée,59030,Montérégie,plex,1.0,1,1.5,not_published
2321 +Saint-Arsène,12065,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2322 +Irlande,31040,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2323 +Berthierville,52035,Lanaudière,condo,1.0,1,1.5,not_published
2324 +Pointe-Calumet,72020,Laurentides,condo,1.0,1,1.5,not_published
2325 +Saint-Edmond-les-Plaines,92050,Saguenay–Lac-Saint-Jean,plex,1.0,1,1.5,not_published
2326 +Rivière-aux-Outardes,96902,Côte-Nord,plex,1.0,1,1.5,not_published
2327 +Stukely-Sud,45105,Estrie,plex,1.0,1,1.5,not_published
2328 +Sainte-Sabine,28065,Chaudière-Appalaches,plex,1.0,1,1.5,not_published
2329 +Baie-Trinité,96005,Côte-Nord,plex,1.0,1,1.5,not_published
2330 +Lac-Walker,97904,Côte-Nord,plex,1.0,1,1.5,not_published
2331 +Saint-Pie,54008,Montérégie,condo,1.0,1,1.5,not_published
2332 +Baie-Sainte-Catherine,15065,Capitale-Nationale,plex,1.0,1,1.5,not_published
2333 +Lac-Chicobi,88904,Abitibi-Témiscamingue,plex,1.0,1,1.5,not_published
2334 +Lac-Édouard,90027,Mauricie,plex,1.0,1,1.5,not_published
2335 +Grand-Métis,09060,Bas-Saint-Laurent,plex,1.0,1,1.5,not_published
2336 +Saint-Cléophas-de-Brandon,52075,Lanaudière,plex,1.0,1,1.5,not_published
added outputs/tables/downsampling_monthly.csv +7 −0
@@ -0,0 +1,7 @@
1 +target_tx_per_month,rmse_direct_pct,rmse_hierarchical_pct
2 +300,1.01,1.3
3 +150,1.2,1.37
4 +75,1.49,1.31
5 +40,1.85,1.45
6 +20,2.09,1.69
7 +10,2.59,2.21
added outputs/tables/downsampling_results.csv +7 −0
@@ -0,0 +1,7 @@
1 +target_tx_per_week,rmse_log_pct,bias_log_pct,volatility_ratio,turning_point_agreement,ci95_coverage
2 +100,0.317,-0.046,1.051,0.861,1.0
3 +50,0.636,-0.177,0.987,0.799,0.992
4 +25,0.799,0.002,0.762,0.771,0.977
5 +15,1.15,-0.154,0.449,0.778,0.881
6 +10,1.361,-0.18,0.358,0.735,0.828
7 +5,1.633,0.189,0.457,0.685,0.856
added outputs/tables/estimation_methods.csv +85 −0
@@ -0,0 +1,85 @@
1 +cell,method,median_tx
2 +quebec/unifamilial,rtd_mean_splice,
3 +region-01/unifamilial,direct_local_td,188.0
4 +region-02/unifamilial,direct_local_td,255.0
5 +region-03/unifamilial,direct_local_td,543.0
6 +region-04/unifamilial,direct_local_td,254.0
7 +region-05/unifamilial,direct_local_td,488.0
8 +region-06/unifamilial,direct_local_td,401.0
9 +region-07/unifamilial,direct_local_td,406.0
10 +region-08/unifamilial,direct_local_td,125.0
11 +region-09/unifamilial,direct_local_td,67.0
12 +region-10/unifamilial,hierarchical,16.0
13 +region-11/unifamilial,direct_local_td,80.0
14 +region-12/unifamilial,direct_local_td,408.0
15 +region-13/unifamilial,direct_local_td,234.0
16 +region-14/unifamilial,direct_local_td,543.0
17 +region-15/unifamilial,direct_local_td,689.0
18 +region-16/unifamilial,direct_local_td,1181.0
19 +region-17/unifamilial,direct_local_td,226.0
20 +montreal/unifamilial,direct_local_td,280.0
21 +quebec-city/unifamilial,direct_local_td,303.0
22 +laval/unifamilial,direct_local_td,234.0
23 +gatineau/unifamilial,direct_local_td,240.0
24 +longueuil/unifamilial,direct_local_td,118.0
25 +sherbrooke/unifamilial,direct_local_td,118.0
26 +trois-rivieres/unifamilial,direct_local_td,101.0
27 +saguenay/unifamilial,direct_local_td,117.0
28 +levis/unifamilial,direct_local_td,111.0
29 +drummondville/unifamilial,direct_local_td,72.0
30 +quebec/condo,rtd_mean_splice,
31 +region-01/condo,hierarchical,3.0
32 +region-02/condo,hierarchical,5.0
33 +region-03/condo,direct_local_td,186.0
34 +region-04/condo,hierarchical,5.0
35 +region-05/condo,hierarchical,27.0
36 +region-06/condo,direct_local_td,659.0
37 +region-07/condo,hierarchical,30.0
38 +region-08/condo,hierarchical,1.0
39 +region-09/condo,hierarchical,0.0
40 +region-10/condo,hierarchical,0.0
41 +region-11/condo,hierarchical,0.0
42 +region-12/condo,hierarchical,11.0
43 +region-13/condo,direct_local_td,91.0
44 +region-14/condo,direct_local_td,47.0
45 +region-15/condo,direct_local_td,45.0
46 +region-16/condo,direct_local_td,155.0
47 +region-17/condo,hierarchical,2.0
48 +montreal/condo,direct_local_td,620.0
49 +quebec-city/condo,direct_local_td,173.0
50 +laval/condo,direct_local_td,91.0
51 +gatineau/condo,hierarchical,30.0
52 +longueuil/condo,direct_local_td,60.0
53 +sherbrooke/condo,hierarchical,7.0
54 +trois-rivieres/condo,hierarchical,4.0
55 +saguenay/condo,hierarchical,4.0
56 +levis/condo,hierarchical,9.0
57 +drummondville/condo,hierarchical,1.0
58 +quebec/plex,rtd_mean_splice,
59 +region-01/plex,hierarchical,24.0
60 +region-02/plex,direct_local_td,57.0
61 +region-03/plex,direct_local_td,115.0
62 +region-04/plex,direct_local_td,68.0
63 +region-05/plex,direct_local_td,83.0
64 +region-06/plex,direct_local_td,351.0
65 +region-07/plex,direct_local_td,70.0
66 +region-08/plex,hierarchical,23.0
67 +region-09/plex,hierarchical,11.0
68 +region-10/plex,hierarchical,2.0
69 +region-11/plex,hierarchical,8.0
70 +region-12/plex,direct_local_td,55.0
71 +region-13/plex,hierarchical,34.0
72 +region-14/plex,direct_local_td,70.0
73 +region-15/plex,direct_local_td,130.0
74 +region-16/plex,direct_local_td,163.0
75 +region-17/plex,hierarchical,35.0
76 +montreal/plex,direct_local_td,346.0
77 +quebec-city/plex,direct_local_td,92.0
78 +laval/plex,hierarchical,34.0
79 +gatineau/plex,direct_local_td,52.0
80 +longueuil/plex,direct_local_td,40.0
81 +sherbrooke/plex,hierarchical,34.0
82 +trois-rivieres/plex,hierarchical,34.0
83 +saguenay/plex,hierarchical,37.0
84 +levis/plex,hierarchical,22.0
85 +drummondville/plex,hierarchical,16.0
added outputs/tables/exclusions.csv +7 −0
@@ -0,0 +1,7 @@
1 +exclude_reason,n,pct_of_raw
2 +assessment_ratio_outside_band,24976,3.352
3 +exact_duplicate,665,0.089
4 +addr_date_amount_duplicate,107,0.014
5 +price_above_10m,46,0.006
6 +TOTAL_EXCLUDED,25794,3.462
7 +RETAINED_IN_RESEARCH_SAMPLE,719325,96.538
added outputs/tables/geo_city_mismatch_top300.csv +8 −0
@@ -0,0 +1,8 @@
1 +city,municipality,region,n
2 +Saint-Bruno,Hébertville,Saguenay–Lac-Saint-Jean,292
3 +Courcelles-Saint-Évariste,Courcelles–Saint-Évariste,Chaudière-Appalaches,140
4 +Saint-Félix-de-Dalquier,Amos,Abitibi-Témiscamingue,95
5 +Saint-Onésime-d'Ixworth,La Pocatière,Bas-Saint-Laurent,61
6 +Passes-Dangereuses,Sainte-Élisabeth-de-Proulx,Saguenay–Lac-Saint-Jean,16
7 +Saint-Guy,Lac-des-Aigles,Bas-Saint-Laurent,13
8 +TNO aquatique de la MRC d'Argenteuil,Grenville-sur-la-Rouge,Laurentides,1
added outputs/tables/geo_region_distribution.csv +18 −0
@@ -0,0 +1,18 @@
1 +region_code,region,n_transactions
2 +16,Montérégie,134113
3 +06,Montréal,116682
4 +15,Laurentides,77374
5 +03,Capitale-Nationale,73093
6 +14,Lanaudière,57064
7 +05,Estrie,52133
8 +07,Outaouais,44087
9 +12,Chaudière-Appalaches,42729
10 +13,Laval,28651
11 +02,Saguenay–Lac-Saint-Jean,26725
12 +04,Mauricie,26218
13 +17,Centre-du-Québec,22995
14 +01,Bas-Saint-Laurent,17245
15 +08,Abitibi-Témiscamingue,12273
16 +11,Gaspésie–Îles-de-la-Madeleine,6452
17 +09,Côte-Nord,5911
18 +10,Nord-du-Québec,1374
added outputs/tables/hierarchical_summary.csv +113 −0
@@ -0,0 +1,113 @@
1 +geography,property_type,median_weekly_tx,sd_raw_pct,sd_smoothed_pct,mean_shrinkage,last_index
2 +drummondville,all,20.0,11.39,7.37,0.977,171.7
3 +drummondville,condo,0.0,24.23,4.17,0.978,195.26
4 +drummondville,plex,4.0,29.79,7.97,0.98,198.35
5 +drummondville,unifamilial,15.0,19.08,8.24,0.977,165.58
6 +gatineau,all,74.0,6.39,3.08,0.934,147.17
7 +gatineau,condo,6.0,13.78,3.81,0.977,151.59
8 +gatineau,plex,12.0,15.76,5.13,0.956,164.61
9 +gatineau,unifamilial,54.0,8.15,3.63,0.923,143.1
10 +laval,all,84.0,5.99,3.76,0.977,146.54
11 +laval,condo,20.0,9.82,3.71,0.978,159.74
12 +laval,plex,8.0,25.58,5.31,0.98,168.93
13 +laval,unifamilial,53.0,7.64,5.22,0.976,139.01
14 +levis,all,32.0,11.48,4.98,0.939,155.87
15 +levis,condo,2.0,26.41,5.54,0.976,125.54
16 +levis,plex,5.0,30.31,5.57,0.979,187.34
17 +levis,unifamilial,25.0,14.13,5.76,0.929,154.55
18 +longueuil,all,51.0,7.98,3.41,0.952,143.21
19 +longueuil,condo,14.0,11.11,3.68,0.963,138.52
20 +longueuil,plex,9.0,19.64,5.28,0.956,167.32
21 +longueuil,unifamilial,28.0,12.21,5.19,0.945,138.82
22 +montreal,all,281.0,4.05,4.46,0.968,137.67
23 +montreal,condo,140.0,6.97,4.88,0.972,133.59
24 +montreal,plex,76.0,14.17,8.82,0.974,150.75
25 +montreal,unifamilial,64.0,7.09,3.96,0.951,131.78
26 +quebec,all,1999.0,3.04,3.04,0.0,156.73
27 +quebec,condo,291.0,3.78,3.78,0.0,145.29
28 +quebec,plex,297.0,5.18,5.18,0.0,169.51
29 +quebec,unifamilial,1384.0,3.67,3.67,0.0,156.72
30 +quebec-city,all,121.0,6.84,2.92,0.975,164.19
31 +quebec-city,condo,37.0,9.57,4.16,0.976,159.98
32 +quebec-city,plex,20.0,16.4,5.2,0.979,170.88
33 +quebec-city,unifamilial,64.0,10.25,4.03,0.973,164.74
34 +region-01,all,48.0,9.11,3.45,0.956,161.87
35 +region-01,condo,0.0,26.31,3.93,0.979,155.09
36 +region-01,plex,5.0,27.4,5.25,0.935,173.36
37 +region-01,unifamilial,42.0,13.95,3.71,0.959,160.52
38 +region-02,all,70.0,7.95,3.39,0.882,170.14
39 +region-02,condo,1.0,25.26,3.93,0.966,161.73
40 +region-02,plex,13.0,19.64,5.25,0.933,191.62
41 +region-02,unifamilial,57.0,11.55,3.78,0.869,165.92
42 +region-03,all,180.0,6.74,3.13,0.84,166.74
43 +region-03,condo,40.0,9.07,4.03,0.838,161.75
44 +region-03,plex,26.0,14.94,5.17,0.942,173.74
45 +region-03,unifamilial,115.0,9.89,3.87,0.82,167.1
46 +region-04,all,73.0,8.54,3.88,0.906,190.5
47 +region-04,condo,1.0,29.76,3.92,0.95,212.67
48 +region-04,plex,16.0,15.69,5.76,0.89,221.95
49 +region-04,unifamilial,55.0,11.29,4.11,0.909,181.99
50 +region-05,all,134.0,8.58,3.61,0.944,165.76
51 +region-05,condo,6.0,21.33,3.75,0.963,155.55
52 +region-05,plex,19.0,18.1,5.3,0.951,189.49
53 +region-05,unifamilial,106.0,11.3,4.16,0.941,162.4
54 +region-06,all,328.0,3.8,3.22,0.865,136.31
55 +region-06,condo,151.0,6.76,4.07,0.876,135.38
56 +region-06,plex,78.0,14.13,6.55,0.9,148.53
57 +region-06,unifamilial,96.0,6.07,3.67,0.815,128.03
58 +region-07,all,116.0,6.08,3.2,0.896,148.08
59 +region-07,condo,6.0,14.04,3.69,0.856,148.62
60 +region-07,plex,15.0,16.23,5.22,0.961,165.38
61 +region-07,unifamilial,92.0,6.94,3.62,0.888,145.36
62 +region-08,all,33.0,10.56,3.58,0.957,149.85
63 +region-08,condo,0.0,35.57,4.31,0.97,162.89
64 +region-08,plex,5.0,27.78,5.23,0.972,165.03
65 +region-08,unifamilial,27.0,13.13,3.91,0.954,147.05
66 +region-09,all,18.0,10.47,4.88,0.768,169.05
67 +region-09,condo,0.0,39.91,4.86,0.988,146.6
68 +region-09,plex,2.0,36.61,6.76,0.961,172.45
69 +region-09,unifamilial,15.0,21.43,5.58,0.737,168.67
70 +region-10,all,4.0,NaN,3.66,0.949,128.98
71 +region-10,condo,0.0,NaN,3.78,1.0,145.29
72 +region-10,plex,0.0,55.62,5.23,0.982,155.51
73 +region-10,unifamilial,4.0,39.64,3.89,0.946,126.34
74 +region-11,all,19.0,NaN,3.78,0.958,159.22
75 +region-11,condo,0.0,NaN,3.79,0.997,159.49
76 +region-11,plex,2.0,51.26,5.61,0.98,179.82
77 +region-11,unifamilial,17.0,18.37,3.99,0.955,156.85
78 +region-12,all,106.0,7.69,4.84,0.87,163.39
79 +region-12,condo,3.0,31.69,5.17,0.908,128.46
80 +region-12,plex,12.0,20.52,5.27,0.944,184.33
81 +region-12,unifamilial,92.0,9.57,5.34,0.859,162.41
82 +region-13,all,84.0,5.99,3.13,0.858,147.29
83 +region-13,condo,20.0,9.82,3.67,0.851,158.85
84 +region-13,plex,8.0,25.58,5.2,0.94,167.57
85 +region-13,unifamilial,53.0,7.64,4.13,0.848,140.55
86 +region-14,all,156.0,5.6,3.29,0.859,160.25
87 +region-14,condo,11.0,10.61,4.34,0.849,153.79
88 +region-14,plex,16.0,15.65,5.03,0.966,173.96
89 +region-14,unifamilial,128.0,6.66,3.69,0.846,159.26
90 +region-15,all,201.0,5.44,3.64,0.924,159.77
91 +region-15,condo,10.0,20.89,3.83,0.894,172.41
92 +region-15,plex,29.0,12.25,5.36,0.959,177.2
93 +region-15,unifamilial,160.0,6.45,4.34,0.92,155.87
94 +region-16,all,341.0,4.59,4.39,0.914,150.33
95 +region-16,condo,35.0,8.31,3.66,0.911,141.17
96 +region-16,plex,37.0,10.75,5.21,0.944,173.25
97 +region-16,unifamilial,265.0,9.72,5.34,0.91,148.83
98 +region-17,all,60.0,8.17,6.93,0.938,164.63
99 +region-17,condo,0.0,26.84,3.88,0.966,168.22
100 +region-17,plex,9.0,24.25,6.43,0.946,185.36
101 +region-17,unifamilial,51.0,12.34,7.73,0.937,161.6
102 +saguenay,all,34.0,9.54,3.35,0.97,173.52
103 +saguenay,condo,1.0,25.29,4.41,0.979,158.96
104 +saguenay,plex,8.0,18.75,5.41,0.978,191.9
105 +saguenay,unifamilial,25.0,13.22,3.79,0.967,169.0
106 +sherbrooke,all,37.0,11.85,3.35,0.958,166.84
107 +sherbrooke,condo,1.0,40.65,4.19,0.954,150.57
108 +sherbrooke,plex,7.0,27.71,5.27,0.98,190.34
109 +sherbrooke,unifamilial,29.0,13.32,3.69,0.952,162.12
110 +trois-rivieres,all,32.0,10.2,4.0,0.962,195.79
111 +trois-rivieres,condo,1.0,30.42,4.16,0.98,223.27
112 +trois-rivieres,plex,8.0,19.82,6.56,0.978,223.32
113 +trois-rivieres,unifamilial,23.0,14.4,4.14,0.955,185.82
added outputs/tables/index_latest.csv +113 −0
@@ -0,0 +1,113 @@
1 +week,geography_level,geography_id,geography_name,property_type,index,index_smoothed,representative_value,transactions,effective_sample_size,weekly_pct,four_week_pct,thirteen_week_pct,twenty_six_week_pct,yoy_pct,lower_95,upper_95,reliability_grade,shrinkage_weight,is_partial_week,model_version,data_vintage
2 +2026-07-13,municipality,drummondville,Drummondville,all,164.77849134185595,173.4798580605911,437355.0,28,627.2,2.237,-0.462,7.06,11.081,10.967,168.34444769634925,178.77192603944428,B,0.9905026564268633,false,0.1.0,2026-08-06
3 +2026-07-13,municipality,drummondville,Drummondville,condo,162.1855771911848,182.94775566935903,385883.0,1,25.0,-1.16,-2.825,-4.75,4.822,0.358,162.31186840843915,206.20723322728546,E,0.9544812392950297,false,0.1.0,2026-08-06
4 +2026-07-13,municipality,drummondville,Drummondville,plex,168.62206897926285,184.38551329576697,517592.0,3,126.0,3.043,1.126,4.049,7.691,1.876,171.29983556126166,198.4708123154316,E,0.9974982064019289,false,0.1.0,2026-08-06
5 +2026-07-13,municipality,drummondville,Drummondville,unifamilial,163.9292539802428,170.84234646821383,421118.0,24,591.4,2.108,-0.791,8.004,12.01,13.428,165.23813679413064,176.63662828109275,C,0.9894791980422197,false,0.1.0,2026-08-06
6 +2026-07-13,municipality,gatineau,Gatineau,all,151.52040999080805,147.4388747429021,495190.0,94,674.3,2.327,-0.199,2.671,4.264,3.013,143.22721739268897,151.77437767189915,A,0.932973713106922,false,0.1.0,2026-08-06
7 +2026-07-13,municipality,gatineau,Gatineau,condo,143.922212828847,144.744083289835,316144.0,3,51.7,-0.578,-2.473,-5.748,3.077,-1.559,133.17419760756493,157.31913556672777,D,0.9988016332665152,false,0.1.0,2026-08-06
8 +2026-07-13,municipality,gatineau,Gatineau,plex,168.84107844244272,153.91636378143494,626749.0,19,194.8,4.379,1.685,2.627,2.448,2.584,145.06995617739946,163.30222786259952,C,0.9504109722556551,false,0.1.0,2026-08-06
9 +2026-07-13,municipality,gatineau,Gatineau,unifamilial,149.19932698131655,146.49496368578122,502825.0,72,529.1,2.326,-0.259,3.97,4.828,3.789,141.41906588379342,151.75304865139654,B,0.9196699204008252,false,0.1.0,2026-08-06
10 +2026-07-13,municipality,laval,Laval,all,134.4609200037922,145.2168764962385,580987.0,86,668.9,0.148,-3.292,-1.178,2.845,4.592,141.0524091956634,149.50429659142702,A,0.9965668562430385,false,0.1.0,2026-08-06
11 +2026-07-13,municipality,laval,Laval,condo,141.51152347856134,150.98566600881333,420877.0,13,106.2,-1.076,-5.809,-7.583,-0.842,-1.374,142.46471629218814,160.01626180457248,C,0.9978965986604492,false,0.1.0,2026-08-06
12 +2026-07-13,municipality,laval,Laval,plex,164.32506009215183,158.20718498585975,845842.0,8,143.8,3.764,2.52,11.014,10.568,9.215,147.67255541699427,169.49333144857079,D,0.9966949830749318,false,0.1.0,2026-08-06
13 +2026-07-13,municipality,laval,Laval,unifamilial,128.14383835810253,141.35845415953307,620008.0,65,476.2,0.091,-3.161,-0.377,3.162,6.236,136.20060738435302,146.71162593264916,B,0.996053320535233,false,0.1.0,2026-08-06
14 +2026-07-13,municipality,levis,Lévis,all,165.1686271634607,157.52717110255958,428858.0,62,436.3,2.333,0.425,10.706,10.04,10.376,151.9529627959471,163.30586241281867,B,0.9417514804812133,false,0.1.0,2026-08-06
15 +2026-07-13,municipality,levis,Lévis,condo,137.86269578040984,119.85427494742905,286323.0,3,40.8,0.557,-1.309,4.675,15.582,13.667,109.12671305399108,131.63639608632508,E,0.9967287464646899,false,0.1.0,2026-08-06
16 +2026-07-13,municipality,levis,Lévis,plex,172.6868961867643,175.84032940959102,526571.0,13,158.0,4.103,5.176,9.996,8.928,5.932,164.65573506839436,187.78466133615126,D,0.9913713993739749,false,0.1.0,2026-08-06
17 +2026-07-13,municipality,levis,Lévis,unifamilial,166.94614733046757,158.98448129802284,433201.0,46,352.1,2.224,-0.159,11.443,9.685,10.796,152.25884382367704,166.00720627349733,B,0.9281560081720617,false,0.1.0,2026-08-06
18 +2026-07-13,municipality,longueuil,Longueuil,all,135.37495568216426,141.68901488544614,547703.0,47,858.1,1.254,-0.702,4.495,6.275,4.086,138.0953343064277,145.37621448282155,A,0.969439212880761,false,0.1.0,2026-08-06
19 +2026-07-13,municipality,longueuil,Longueuil,condo,118.1831940015355,131.68072261082753,367520.0,8,159.9,-0.939,-1.714,-3.444,4.663,0.137,125.59147110592039,138.06520900360968,C,0.9872499053820072,false,0.1.0,2026-08-06
20 +2026-07-13,municipality,longueuil,Longueuil,plex,111.08190049998647,156.34181658510758,806829.0,9,154.2,2.087,0.103,6.008,4.531,0.088,146.27780849356807,167.09823496026863,D,0.9698507147970352,false,0.1.0,2026-08-06
21 +2026-07-13,municipality,longueuil,Longueuil,unifamilial,154.76361801267538,142.81582507109277,599239.0,30,558.4,2.172,-0.415,8.467,7.695,7.551,137.99690219768829,147.80302721229233,B,0.9598856099024536,false,0.1.0,2026-08-06
22 +2026-07-13,municipality,montreal,Montréal,all,133.06986553548586,132.76064368136556,633421.0,309,822.3,1.153,-1.178,-0.099,2.911,1.024,129.32191793575694,136.29080663222337,A,0.9887158395063785,false,0.1.0,2026-08-06
23 +2026-07-13,municipality,montreal,Montréal,condo,125.42430368011634,126.92022864212704,488767.0,125,215.2,-0.592,-2.577,-2.927,2.97,-2.119,121.84395502214286,132.20799042219485,B,0.9970729871173344,false,0.1.0,2026-08-06
24 +2026-07-13,municipality,montreal,Montréal,plex,142.95638717921628,141.21969257393548,855882.0,102,263.2,3.708,1.968,3.88,3.936,2.587,134.20777785936548,148.5979567560894,C,0.9956826377453092,false,0.1.0,2026-08-06
25 +2026-07-13,municipality,montreal,Montréal,unifamilial,139.54576158598798,136.40111214250186,790109.0,82,478.8,2.084,-1.777,1.691,1.511,6.627,131.43731662129838,141.5523678661021,B,0.9607016997642371,false,0.1.0,2026-08-06
26 +2026-07-13,municipality,quebec-city,Québec,all,166.2505397958448,162.90258077909542,447620.0,173,974.1,1.721,0.324,8.036,9.635,10.723,159.02172297770417,166.87814927153312,A,0.9948373151056271,false,0.1.0,2026-08-06
27 +2026-07-13,municipality,quebec-city,Québec,condo,152.01169074222744,152.69568762449413,328294.0,55,141.7,-0.357,-1.004,-1.494,8.095,7.405,145.20487301877998,160.57293763206923,C,0.9921405685245888,false,0.1.0,2026-08-06
28 +2026-07-13,municipality,quebec-city,Québec,plex,166.57402899830836,159.3589194193802,575230.0,25,222.1,3.735,2.485,8.112,6.946,9.285,150.76467334208868,168.44307512868104,C,0.996176799613064,false,0.1.0,2026-08-06
29 +2026-07-13,municipality,quebec-city,Québec,unifamilial,174.65966395092127,169.91987433305448,495439.0,93,703.5,2.337,0.474,13.717,11.27,13.027,164.80223693570352,175.1964307658369,B,0.9959667091066711,false,0.1.0,2026-08-06
30 +2026-07-13,municipality,saguenay,Saguenay,all,NaN,173.88222851845464,368933.0,62,560.3,2.222,-0.232,6.659,13.803,12.478,168.44111214172003,179.4991080865428,B,0.9767235420261182,false,0.1.0,2026-08-06
31 +2026-07-13,municipality,saguenay,Saguenay,condo,NaN,150.5139860769468,270447.0,0,59.0,0.189,-1.324,-1.432,6.277,3.94,139.22824367895774,162.7145427260404,E,1.0,false,0.1.0,2026-08-06
32 +2026-07-13,municipality,saguenay,Saguenay,plex,193.98266629730384,179.37992410862378,389319.0,10,166.1,3.635,6.08,6.997,12.303,11.034,168.24061365681519,191.25677488820892,D,0.9961027747992764,false,0.1.0,2026-08-06
33 +2026-07-13,municipality,saguenay,Saguenay,unifamilial,177.26410039299967,173.21138600603447,367435.0,52,461.3,1.885,-1.976,6.868,14.535,13.233,166.79196813970313,179.87787167906055,B,0.9701711220428698,false,0.1.0,2026-08-06
34 +2026-07-13,municipality,sherbrooke,Sherbrooke,all,168.3080121946426,167.2882163217376,451680.0,67,643.1,1.94,-1.315,7.846,8.814,10.04,162.39679702241122,172.32696600688755,B,0.959393955057428,false,0.1.0,2026-08-06
35 +2026-07-13,municipality,sherbrooke,Sherbrooke,condo,140.31759528284837,141.86039648082988,319123.0,4,27.1,-0.71,-5.963,-3.602,5.634,-1.439,126.44462745842087,159.15561217747901,E,0.9198263514654395,false,0.1.0,2026-08-06
36 +2026-07-13,municipality,sherbrooke,Sherbrooke,plex,196.0650747383211,178.05995340871354,564836.0,9,188.2,3.578,1.795,7.984,8.241,6.743,167.6538299674035,189.11197563501904,D,0.9962387344161051,false,0.1.0,2026-08-06
37 +2026-07-13,municipality,sherbrooke,Sherbrooke,unifamilial,163.36244957209058,166.06822213010776,434345.0,54,532.1,1.663,-1.852,8.463,9.137,11.572,160.3303132968328,172.01147951601737,B,0.9519620864180663,false,0.1.0,2026-08-06
38 +2026-07-13,municipality,trois-rivieres,Trois-Rivières,all,NaN,195.6099747683359,409408.0,45,413.6,2.94,-0.082,11.727,10.332,9.313,188.50418154021773,202.98362570119147,B,0.9737231381213693,false,0.1.0,2026-08-06
39 +2026-07-13,municipality,trois-rivieres,Trois-Rivières,condo,NaN,212.3090456144626,312619.0,0,17.5,-0.439,-1.096,-3.699,9.244,3.533,184.02055399111643,244.94617515334704,E,1.0,false,0.1.0,2026-08-06
40 +2026-07-13,municipality,trois-rivieres,Trois-Rivières,plex,231.35986878875804,209.28385662746734,458237.0,13,116.5,4.972,3.99,14.991,7.822,7.426,193.8640633652886,225.9301279698064,D,0.9948459700470538,false,0.1.0,2026-08-06
41 +2026-07-13,municipality,trois-rivieres,Trois-Rivières,unifamilial,197.12580100155935,190.28811154215802,398819.0,32,343.0,2.421,-1.401,11.466,11.27,10.28,182.1336935073014,198.8076159715566,B,0.9650659349310294,false,0.1.0,2026-08-06
42 +2026-07-13,province,quebec,Québec (province),all,157.0103100782035,157.0103100782035,469013.0,2487,2302.0,1.672,-0.827,5.891,7.644,7.604,154.56682412570223,159.49242413627556,A,0.0,false,0.1.0,2026-08-06
43 +2026-07-13,province,quebec,Québec (province),condo,138.16020078426592,138.16020078426592,419543.0,258,258.0,-0.439,-2.121,-2.366,4.956,0.859,133.10497997669714,143.4074148397038,B,0.0,false,0.1.0,2026-08-06
44 +2026-07-13,province,quebec,Québec (province),plex,158.89405583388057,158.89405583388057,583567.0,364,364.0,3.566,2.405,6.527,6.161,4.765,152.16014393276552,165.9259798708938,B,0.0,false,0.1.0,2026-08-06
45 +2026-07-13,province,quebec,Québec (province),unifamilial,161.0061781039289,161.0061781039289,458587.0,1865,1865.0,1.738,-1.212,7.63,8.55,9.734,158.01040415933193,164.0587499636687,A,0.0,false,0.1.0,2026-08-06
46 +2026-07-13,region,region-01,Bas-Saint-Laurent,all,168.54248122855066,164.21468945413278,269166.0,74,839.8,1.873,-1.235,7.232,7.436,9.762,160.00521165026916,168.53491179686773,B,0.9588173040003705,false,0.1.0,2026-08-06
47 +2026-07-13,region,region-01,Bas-Saint-Laurent,condo,153.57434800733495,147.47607771388306,267384.0,2,94.4,-0.336,-1.612,-2.097,5.581,2.887,138.66340498248582,156.84883477813307,E,0.9751415721053844,false,0.1.0,2026-08-06
48 +2026-07-13,region,region-01,Bas-Saint-Laurent,plex,141.27616345425957,163.14763948121507,303239.0,12,94.9,1.891,2.281,3.671,3.057,5.869,149.88136844423136,177.58813216464847,D,0.8982540467818814,false,0.1.0,2026-08-06
49 +2026-07-13,region,region-01,Bas-Saint-Laurent,unifamilial,172.83111055971344,164.66295519314008,264874.0,60,861.4,1.909,-1.688,7.885,8.066,10.415,160.17453642462633,169.2771486540066,B,0.9666073120009592,false,0.1.0,2026-08-06
50 +2026-07-13,region,region-02,Saguenay–Lac-Saint-Jean,all,173.19199091760845,171.53867483023387,316631.0,116,620.6,2.102,-0.494,7.148,14.096,12.163,166.43419949119036,176.7997025399233,B,0.8685966199199296,false,0.1.0,2026-08-06
51 +2026-07-13,region,region-02,Saguenay–Lac-Saint-Jean,condo,211.25730277068428,153.1324220410915,263521.0,1,65.2,0.189,-1.388,-1.513,6.112,4.017,142.18583527398653,164.92176337385945,E,0.9808352946165771,false,0.1.0,2026-08-06
52 +2026-07-13,region,region-02,Saguenay–Lac-Saint-Jean,plex,179.99037704939064,179.1895414688435,349192.0,21,173.8,3.604,5.78,6.993,12.005,10.798,168.30350135219777,190.7797016333142,C,0.9227909959543318,false,0.1.0,2026-08-06
53 +2026-07-13,region,region-02,Saguenay–Lac-Saint-Jean,unifamilial,171.04030911715947,170.29521360806544,311029.0,94,543.1,1.814,-1.804,7.367,14.729,12.637,164.47006492951704,176.32667555791787,B,0.854407340463168,false,0.1.0,2026-08-06
54 +2026-07-13,region,region-03,Capitale-Nationale,all,170.40825391250712,166.79009386498794,438556.0,271,1017.7,1.897,0.344,9.316,9.939,11.137,162.90152824901543,170.77148207576514,A,0.840225224392434,false,0.1.0,2026-08-06
55 +2026-07-13,region,region-03,Capitale-Nationale,condo,155.00304909422263,154.35746577263413,330220.0,59,144.8,-0.356,-1.042,-1.51,8.034,7.497,146.8641600327146,162.23309508897557,C,0.8337164088776177,false,0.1.0,2026-08-06
56 +2026-07-13,region,region-03,Capitale-Nationale,plex,166.61396961690573,162.06955013914884,531740.0,33,229.8,3.72,2.447,8.036,6.972,9.063,153.47221122353525,171.14850221352685,C,0.9490264659785378,false,0.1.0,2026-08-06
57 +2026-07-13,region,region-03,Capitale-Nationale,unifamilial,176.94446758126145,172.36363393879213,465172.0,179,717.6,2.325,0.407,13.639,11.23,12.868,167.22302395031343,177.66227163439743,B,0.8203313532104751,false,0.1.0,2026-08-06
58 +2026-07-13,region,region-04,Mauricie,all,NaN,191.19381289186285,343264.0,99,558.8,2.804,0.193,11.577,10.342,10.339,185.20313529742154,197.37826808074342,B,0.9014094383701756,false,0.1.0,2026-08-06
59 +2026-07-13,region,region-04,Mauricie,condo,NaN,202.22527363159634,311601.0,0,18.3,-0.439,-1.084,-3.569,9.136,3.487,175.8297936314493,232.58322978580486,E,1.0,false,0.1.0,2026-08-06
60 +2026-07-13,region,region-04,Mauricie,plex,228.77520653974912,207.96115041965382,379379.0,19,120.2,4.918,3.863,14.644,7.672,7.382,192.86853134778005,224.23481830678477,C,0.880263279881631,false,0.1.0,2026-08-06
61 +2026-07-13,region,region-04,Mauricie,unifamilial,196.49119518545507,186.46658385166256,334547.0,80,518.7,2.3,-0.783,11.136,11.135,11.363,179.94275188640165,193.226937616578,B,0.9048735865801549,false,0.1.0,2026-08-06
62 +2026-07-13,region,region-05,Estrie,all,166.94917327062214,167.0842803467695,444165.0,192,1091.4,1.906,-1.087,7.089,7.815,9.383,163.32116808680556,170.9340991497185,A,0.9485094975890755,false,0.1.0,2026-08-06
63 +2026-07-13,region,region-05,Estrie,condo,139.30851354926256,147.6687915171472,334177.0,7,117.1,-0.602,-3.238,-3.693,3.069,0.366,139.7200539489596,156.069736389456,D,0.9726277725939869,false,0.1.0,2026-08-06
64 +2026-07-13,region,region-05,Estrie,plex,176.22665678708356,177.13946436501132,499608.0,25,203.5,3.541,1.802,7.741,7.96,6.081,167.17134385603717,187.701965610477,C,0.9559933391189436,false,0.1.0,2026-08-06
65 +2026-07-13,region,region-05,Estrie,unifamilial,167.03049937443868,166.49295441098454,441904.0,160,974.7,1.757,-1.476,7.616,8.064,10.524,162.22322337502868,170.87506518357804,A,0.9457987319223433,false,0.1.0,2026-08-06
66 +2026-07-13,region,region-06,Montréal,all,131.9093422927083,132.1571119240889,659913.0,366,923.9,1.171,-1.254,0.067,2.68,1.011,128.92531500221105,135.46992095242604,A,0.8779086681219483,false,0.1.0,2026-08-06
67 +2026-07-13,region,region-06,Montréal,condo,126.59188723996262,128.62162155669233,492502.0,133,216.4,-0.588,-2.566,-2.926,2.986,-2.135,123.49204174960842,133.9642724947127,B,0.9138723711345551,false,0.1.0,2026-08-06
68 +2026-07-13,region,region-06,Montréal,plex,140.9571799603617,139.148903470808,858976.0,102,266.4,3.703,1.991,3.952,3.939,2.57,132.2807012371294,146.37371253739218,C,0.9069438168465952,false,0.1.0,2026-08-06
69 +2026-07-13,region,region-06,Montréal,unifamilial,133.40265798473533,132.24510613608257,853912.0,131,581.7,1.97,-1.809,1.85,1.105,5.075,127.87147963660688,136.76832509207227,B,0.7930422464344542,false,0.1.0,2026-08-06
70 +2026-07-13,region,region-07,Outaouais,all,154.56469444680764,149.19966645008276,454631.0,151,850.2,2.207,-0.049,4.465,5.408,4.656,145.39816369137287,153.10056127027116,A,0.8881002962446048,false,0.1.0,2026-08-06
71 +2026-07-13,region,region-07,Outaouais,condo,140.19020917184443,141.90582328306579,317189.0,3,52.1,-0.578,-2.473,-5.734,3.077,-1.513,130.6075622080006,154.18144509561293,D,0.8972171430651947,false,0.1.0,2026-08-06
72 +2026-07-13,region,region-07,Outaouais,plex,169.56363271594213,154.68545767848113,575280.0,21,255.1,3.887,1.823,3.739,3.417,2.58,146.887343547459,162.8975665249894,C,0.9673317152603325,false,0.1.0,2026-08-06
73 +2026-07-13,region,region-07,Outaouais,unifamilial,153.53829354682222,148.97154540551648,451382.0,127,715.1,2.181,-0.138,5.541,5.95,5.57,144.52074927891536,153.55941241127792,B,0.8742055020753409,false,0.1.0,2026-08-06
74 +2026-07-13,region,region-08,Abitibi-Témiscamingue,all,NaN,151.72338029702007,304809.0,58,781.5,1.955,-0.702,5.924,6.492,8.087,147.693651954149,155.8630572416251,B,0.9604291106382046,false,0.1.0,2026-08-06
75 +2026-07-13,region,region-08,Abitibi-Témiscamingue,condo,NaN,156.57533929635366,322052.0,0,10.1,-0.439,-3.315,-4.885,2.249,3.47,129.74838225049362,188.94907551477388,E,1.0,false,0.1.0,2026-08-06
76 +2026-07-13,region,region-08,Abitibi-Témiscamingue,plex,169.17998547460314,154.59222644059895,359022.0,8,204.5,3.766,2.754,5.934,6.109,2.832,145.9131740646789,163.78751698779317,D,0.9790462099558088,false,0.1.0,2026-08-06
77 +2026-07-13,region,region-08,Abitibi-Témiscamingue,unifamilial,147.98939655056404,151.14881240931624,295334.0,50,717.3,1.641,-1.312,6.026,6.603,9.144,146.6399945137583,155.7962653265319,B,0.9565829003172551,false,0.1.0,2026-08-06
78 +2026-07-13,region,region-09,Côte-Nord,all,NaN,164.41804754253917,259337.0,15,65.6,2.289,3.347,17.579,13.554,14.134,149.82897285457886,180.42768259473198,C,0.8124648308981984,false,0.1.0,2026-08-06
79 +2026-07-13,region,region-09,Côte-Nord,condo,NaN,139.40493318989758,235335.0,0,22.1,-0.439,-2.121,-2.26,5.07,-0.048,122.73882041832587,158.33405707700774,E,1.0,false,0.1.0,2026-08-06
80 +2026-07-13,region,region-09,Côte-Nord,plex,179.8012071659903,161.94928217880863,314692.0,2,91.8,3.81,4.554,9.726,13.03,14.075,148.57254507822284,176.53039452492834,E,0.9779968540557293,false,0.1.0,2026-08-06
81 +2026-07-13,region,region-09,Côte-Nord,unifamilial,166.9504636082505,164.9292131828089,251954.0,13,57.9,2.075,3.194,18.921,13.677,14.218,148.25042654177875,183.48443235969154,C,0.7865435865535549,false,0.1.0,2026-08-06
82 +2026-07-13,region,region-10,Nord-du-Québec,all,NaN,130.496607721213,186580.0,4,85.8,3.473,3.158,9.171,12.503,9.935,120.31181052386985,141.54358206890683,E,0.9703470130051219,false,0.1.0,2026-08-06
83 +2026-07-13,region,region-10,Nord-du-Québec,condo,NaN,138.16020078426592,,0,0.0,-0.439,-2.121,-2.366,4.956,0.859,3.7415005674504163,5101.7608407730495,E,1.0,false,0.1.0,2026-08-06
84 +2026-07-13,region,region-10,Nord-du-Québec,plex,69.76327181853792,145.76294492134608,222261.0,1,56.3,2.279,0.944,6.423,2.948,1.56,130.56649878818968,162.72808346198246,E,0.9833122525113603,false,0.1.0,2026-08-06
85 +2026-07-13,region,region-10,Nord-du-Québec,unifamilial,227.33513696344986,128.91067236582518,183115.0,3,82.3,3.608,3.409,9.488,13.615,10.906,117.88545975465752,140.96701564717415,E,0.968894915755899,false,0.1.0,2026-08-06
86 +2026-07-13,region,region-11,Gaspésie–Îles-de-la-Madeleine,all,NaN,161.72051659303742,234668.0,17,369.9,2.336,-0.608,8.345,7.297,8.14,155.51453245968997,168.17415757526066,C,0.9717992139053824,false,0.1.0,2026-08-06
87 +2026-07-13,region,region-11,Gaspésie–Îles-de-la-Madeleine,condo,NaN,151.66195758424809,513224.0,0,1.2,-0.439,-2.121,-2.366,4.956,0.859,87.42240013508922,263.1059012649332,E,1.0,false,0.1.0,2026-08-06
88 +2026-07-13,region,region-11,Gaspésie–Îles-de-la-Madeleine,plex,191.41077633511884,168.74849501011647,294918.0,1,219.1,3.587,2.364,6.69,6.028,4.635,159.58833573331634,178.43443530713208,E,0.9984055704677409,false,0.1.0,2026-08-06
89 +2026-07-13,region,region-11,Gaspésie–Îles-de-la-Madeleine,unifamilial,184.0294785267384,160.87878002075317,228192.0,16,349.1,2.184,-0.967,8.553,7.455,8.581,154.0443478138833,168.0164331133821,C,0.9685219488364073,false,0.1.0,2026-08-06
90 +2026-07-13,region,region-12,Chaudière-Appalaches,all,168.97572493120586,165.76588396587448,321415.0,160,668.3,2.041,0.127,10.335,11.203,12.883,161.00997817257644,170.66226949944354,A,0.8731443296552194,false,0.1.0,2026-08-06
91 +2026-07-13,region,region-12,Chaudière-Appalaches,condo,138.40497669377325,122.62766442209522,275958.0,3,42.0,0.51,-1.255,4.505,15.195,13.257,111.80632638642318,134.49636141022546,E,0.9273408650113658,false,0.1.0,2026-08-06
92 +2026-07-13,region,region-12,Chaudière-Appalaches,plex,189.29199305977608,172.92323840408818,365610.0,18,170.8,4.118,5.002,9.806,9.251,6.086,162.3284126922834,184.209565560414,C,0.9444153739554064,false,0.1.0,2026-08-06
93 +2026-07-13,region,region-12,Chaudière-Appalaches,unifamilial,167.8529319290855,166.80173619062438,318493.0,139,613.2,1.841,-0.418,10.632,11.301,13.757,161.4264883999537,172.35597126583247,B,0.8620751175159382,false,0.1.0,2026-08-06
94 +2026-07-13,region,region-13,Laval,all,135.15240475199326,145.9917924465427,581068.0,86,682.3,0.178,-3.248,-1.128,2.849,4.583,141.8458588600189,150.25890521617413,A,0.8823593322152801,false,0.1.0,2026-08-06
95 +2026-07-13,region,region-13,Laval,condo,140.72967151326026,150.1698316596458,420471.0,13,107.7,-1.062,-5.738,-7.534,-0.784,-1.34,141.74951544145856,159.0903381253514,C,0.9118602910450249,false,0.1.0,2026-08-06
96 +2026-07-13,region,region-13,Laval,plex,163.00209725538434,156.94135543746103,844465.0,8,149.9,3.751,2.513,10.778,10.412,9.138,146.6990308482151,167.89878504399925,D,0.9549336328121784,false,0.1.0,2026-08-06
97 +2026-07-13,region,region-13,Laval,unifamilial,129.56349782464358,142.9587408090533,620482.0,65,486.0,0.133,-3.121,-0.29,3.168,6.218,137.79476405718208,148.31624201068377,B,0.8608502527335418,false,0.1.0,2026-08-06
98 +2026-07-13,region,region-14,Lanaudière,all,147.3355704081245,162.7901209743806,468367.0,170,821.5,0.042,-3.426,3.4,5.892,7.467,158.57157594690142,167.12089369489115,A,0.8808422559099539,false,0.1.0,2026-08-06
99 +2026-07-13,region,region-14,Lanaudière,condo,146.6790451946651,148.0776687898357,351393.0,8,108.4,-0.537,-2.178,0.672,2.643,-1.61,139.8018833231422,156.84335198510584,C,0.9059025355331677,false,0.1.0,2026-08-06
100 +2026-07-13,region,region-14,Lanaudière,plex,182.42316569592862,163.4148805937438,612137.0,18,272.2,3.8,2.679,6.978,5.163,4.87,155.4331325448314,171.8065045865637,C,0.9798909600001908,false,0.1.0,2026-08-06
101 +2026-07-13,region,region-14,Lanaudière,unifamilial,143.51400540052097,164.2283422746672,465573.0,144,685.2,-0.362,-4.283,3.23,6.307,8.731,159.21757470236162,169.39680469762354,B,0.8660002176478554,false,0.1.0,2026-08-06
102 +2026-07-13,region,region-15,Laurentides,all,158.062724878413,160.9645449039262,519981.0,240,1236.9,1.811,-0.862,6.582,7.782,8.462,157.5569406884317,164.44584797672726,A,0.9339461206730794,false,0.1.0,2026-08-06
103 +2026-07-13,region,region-15,Laurentides,condo,209.18343558377717,163.38220171402358,368878.0,6,66.8,1.337,-0.627,-2.128,7.795,5.254,151.83999425474224,175.80179693722687,C,0.9332150505565051,false,0.1.0,2026-08-06
104 +2026-07-13,region,region-15,Laurentides,plex,147.7962528910968,166.37632536565374,610623.0,32,285.3,3.225,2.578,7.363,7.175,5.938,158.43349172937002,174.71736146206786,B,0.9729614332792248,false,0.1.0,2026-08-06
105 +2026-07-13,region,region-15,Laurentides,unifamilial,156.80287426309616,159.80996222823012,516969.0,202,1071.5,1.586,-1.503,7.101,7.894,9.173,155.8987912702034,163.81925619374314,A,0.9267874949068061,false,0.1.0,2026-08-06
106 +2026-07-13,region,region-16,Montérégie,all,150.05984251432022,151.54413253270658,547870.0,377,1601.5,1.665,-1.025,6.265,7.468,6.318,148.72096907231403,154.42088797796725,A,0.9337469049565295,false,0.1.0,2026-08-06
107 +2026-07-13,region,region-16,Montérégie,condo,125.50316488866375,134.17107014767828,382029.0,22,185.3,-0.799,-1.973,-3.081,4.839,-0.108,128.39803559004562,140.20367197867665,B,0.948485569320812,false,0.1.0,2026-08-06
108 +2026-07-13,region,region-16,Montérégie,plex,146.19746159103948,162.22330018428994,633599.0,32,263.8,3.18,1.627,7.503,6.544,3.593,154.1777763432653,170.68866698460323,C,0.9653855486379822,false,0.1.0,2026-08-06
109 +2026-07-13,region,region-16,Montérégie,unifamilial,154.63719080134615,152.87942893996885,566845.0,323,1334.7,1.83,-1.24,7.556,7.99,7.686,149.52260888483707,156.31161044690091,A,0.9272504524799517,false,0.1.0,2026-08-06
110 +2026-07-13,region,region-17,Centre-du-Québec,all,174.3049010662432,167.3697551190376,360317.0,91,782.0,2.242,-0.542,7.448,11.125,11.614,162.92570222083097,171.93502649838535,A,0.9402073570716116,false,0.1.0,2026-08-06
111 +2026-07-13,region,region-17,Centre-du-Québec,condo,154.30597394705478,158.5272050958978,301650.0,1,43.4,-0.548,-2.224,-3.599,5.138,0.651,144.74919049658453,173.616686002191,E,0.9608662505253012,false,0.1.0,2026-08-06
112 +2026-07-13,region,region-17,Centre-du-Québec,plex,157.6485288628178,172.43709844656433,432343.0,11,136.8,3.068,1.236,4.019,7.439,1.987,160.6769566028207,185.05797937268204,D,0.9489961300288267,false,0.1.0,2026-08-06
113 +2026-07-13,region,region-17,Centre-du-Québec,unifamilial,177.28937185060437,166.70782342076032,350898.0,79,760.5,2.149,-0.793,8.127,11.78,13.32,161.8760388698226,171.68383031683123,B,0.9386025767016043,false,0.1.0,2026-08-06
added outputs/tables/index_latest_monthly.csv +113 −0
@@ -0,0 +1,113 @@
1 +period,geography_level,geography_id,geography_name,property_type,index,index_smoothed,representative_value,transactions,effective_sample_size,monthly_pct,three_month_pct,six_month_pct,yoy_pct,lower_95,upper_95,reliability_grade,shrinkage_weight,is_partial_month,model_version,data_vintage
2 +2026-06,municipality,drummondville,Drummondville,all,NaN,177.1234670762736,431834.0,159,349.2,2.177,6.376,7.048,11.859,170.08770271606934,184.45026940889937,B,0.5221224867887166,false,2.1.0,2026-08-08
3 +2026-06,municipality,drummondville,Drummondville,condo,NaN,179.46671810529443,388748.0,0,228.9,-1.235,1.293,1.692,5.436,172.4779520388189,186.73866732970154,E,1.0,false,2.1.0,2026-08-08
4 +2026-06,municipality,drummondville,Drummondville,plex,211.53635132057406,198.82611737378693,536415.0,24,165.2,3.876,8.545,9.947,10.402,186.44236910985592,212.03241054420351,D,0.8302202026417131,false,2.1.0,2026-08-08
5 +2026-06,municipality,drummondville,Drummondville,unifamilial,174.8135147834323,172.31596709831166,411522.0,135,287.9,1.84,5.96,6.471,12.322,164.16092346093552,180.87612990366895,C,0.441051004367348,false,2.1.0,2026-08-08
6 +2026-06,municipality,gatineau,Gatineau,all,143.10997624581967,142.97402503787112,486661.0,413,1275.2,1.361,3.915,3.412,2.902,139.97335287615772,146.03902396776624,A,0.3337282203152186,false,2.1.0,2026-08-08
7 +2026-06,municipality,gatineau,Gatineau,condo,137.97422486374253,143.29612872790238,319003.0,13,1523.2,-2.048,0.135,1.283,1.358,141.10641996367755,145.51981769283927,D,0.9948160665003976,false,2.1.0,2026-08-08
8 +2026-06,municipality,gatineau,Gatineau,plex,150.91659032219277,149.66836088398205,612714.0,57,250.4,0.899,-0.783,-0.897,5.979,142.05104882106983,157.69414189904452,C,0.4814410297245866,false,2.1.0,2026-08-08
9 +2026-06,municipality,gatineau,Gatineau,unifamilial,142.26402253774256,141.54701113197092,492712.0,343,964.2,1.967,5.509,4.667,2.487,137.846095779536,145.34728928730883,A,0.20593092195598373,false,2.1.0,2026-08-08
10 +2026-06,municipality,laval,Laval,all,141.22298504978542,140.39273123148232,573899.0,603,1454.1,0.166,2.748,4.453,3.987,137.63153060454246,143.209327804894,A,0.3784154721767792,false,2.1.0,2026-08-08
11 +2026-06,municipality,laval,Laval,condo,139.240009994328,139.3897334694248,419425.0,123,388.9,-0.171,-0.008,0.997,1.927,135.20563942042938,143.70330912204182,B,0.38554258005590714,false,2.1.0,2026-08-08
12 +2026-06,municipality,laval,Laval,plex,161.449540944382,150.52793524923115,828977.0,49,1253.9,3.619,8.117,9.689,8.9,147.0550013411198,154.0828879246072,D,0.9795937573690241,false,2.1.0,2026-08-08
13 +2026-06,municipality,laval,Laval,unifamilial,139.23833790936027,139.3517295315039,611137.0,431,881.1,-0.2,3.028,5.022,4.065,135.542535645367,143.2679743739557,A,0.2885598860705171,false,2.1.0,2026-08-08
14 +2026-06,municipality,levis,Lévis,all,169.03474484070975,164.92881626390962,431574.0,374,668.6,3.188,10.534,10.264,10.314,160.16754603910468,169.8316238645078,B,0.3186769547180097,false,2.1.0,2026-08-08
15 +2026-06,municipality,levis,Lévis,condo,159.5986670492223,141.89288575590584,301006.0,13,590.9,3.348,9.103,13.847,18.313,138.42776837962717,145.4447417870941,E,0.9760558627266851,false,2.1.0,2026-08-08
16 +2026-06,municipality,levis,Lévis,plex,168.33003164180468,154.68414437409376,489303.0,31,338.7,4.423,5.37,8.479,6.501,147.89098162430955,161.7893414321111,D,0.8968887915980146,false,2.1.0,2026-08-08
17 +2026-06,municipality,levis,Lévis,unifamilial,170.1204666837815,169.18425183941594,439141.0,330,516.9,2.971,11.548,10.208,10.18,163.1719657501608,175.41806853200032,B,0.1585849493030319,false,2.1.0,2026-08-08
18 +2026-06,municipality,longueuil,Longueuil,all,140.77703236433928,138.8702440339345,540318.0,398,972.7,2.278,5.152,4.946,4.883,135.5382660595621,142.28413302535375,A,0.38740743777458697,false,2.1.0,2026-08-08
19 +2026-06,municipality,longueuil,Longueuil,condo,136.38978273802638,136.63679466974713,373639.0,90,340.6,-0.293,1.14,1.89,1.315,132.25881569028647,141.15969177693006,C,0.38146439153664125,false,2.1.0,2026-08-08
20 +2026-06,municipality,longueuil,Longueuil,plex,145.15858504385767,141.53651003634212,778666.0,67,221.9,1.731,3.872,3.493,6.833,133.8974385127458,149.61140329328,C,0.5955486057170623,false,2.1.0,2026-08-08
21 +2026-06,municipality,longueuil,Longueuil,unifamilial,141.7867617525296,139.23444129709432,584896.0,241,482.4,3.839,7.751,7.067,6.207,134.1156043959825,144.5486506258829,B,0.32532780780532755,false,2.1.0,2026-08-08
22 +2026-06,municipality,montreal,Montréal,all,123.9443876072599,123.79468232979207,619320.0,2105,3961.7,0.345,1.848,2.44,2.773,122.31385177350596,125.29344102017448,A,0.2806181551060094,false,2.1.0,2026-08-08
23 +2026-06,municipality,montreal,Montréal,condo,121.61339877490897,121.5584417251484,484520.0,1036,1523.4,0.15,1.94,1.733,0.676,119.7009890105731,123.44471734766627,A,0.23188189307573626,false,2.1.0,2026-08-08
24 +2026-06,municipality,montreal,Montréal,plex,125.99536009727619,126.10647357581537,823545.0,564,932.1,-0.15,1.088,1.804,5.34,122.73816667965963,129.56721701109836,A,0.3693577821590096,false,2.1.0,2026-08-08
25 +2026-06,municipality,montreal,Montréal,unifamilial,126.88519657881866,126.18113566996117,763638.0,505,1122.1,1.422,2.588,4.905,4.527,123.12000334291378,129.31837692219756,A,0.2827012764982748,false,2.1.0,2026-08-08
26 +2026-06,municipality,quebec-city,Québec,all,165.21325218943454,164.21772828306248,450400.0,1278,2383.2,2.598,8.104,7.372,11.36,161.6894067792242,166.7855849039756,A,0.22104482049478913,false,2.1.0,2026-08-08
27 +2026-06,municipality,quebec-city,Québec,condo,164.19661058631374,163.51723041796683,344756.0,377,637.6,1.239,4.876,5.475,10.166,159.67116452117494,167.4559381072002,A,0.25182859655202283,false,2.1.0,2026-08-08
28 +2026-06,municipality,quebec-city,Québec,plex,162.43747312683877,160.99236112074482,575778.0,167,388.7,1.191,6.043,6.019,13.027,154.38267714449358,167.88502971077514,B,0.43015796982643295,false,2.1.0,2026-08-08
29 +2026-06,municipality,quebec-city,Québec,unifamilial,166.56659516813278,165.52124810601376,487647.0,734,1356.3,3.762,10.536,8.828,11.568,161.86467267608873,169.26042676030937,A,0.14565189507075305,false,2.1.0,2026-08-08
30 +2026-06,municipality,saguenay,Saguenay,all,174.14732550787946,173.03031316676888,362298.0,292,799.9,1.109,5.117,11.766,9.938,168.45781977292665,177.7269189103074,A,0.4393655433794468,false,2.1.0,2026-08-08
31 +2026-06,municipality,saguenay,Saguenay,condo,165.15468644189866,162.75594133810523,275673.0,8,494.3,0.575,1.861,3.194,6.407,158.41538907294202,167.2154239299044,E,0.9749255934495725,false,2.1.0,2026-08-08
32 +2026-06,municipality,saguenay,Saguenay,plex,182.00637361962515,177.13203391423582,384920.0,58,1266.9,4.996,2.736,11.204,13.178,173.0659996956915,181.29359604869342,D,0.9771755137740864,false,2.1.0,2026-08-08
33 +2026-06,municipality,saguenay,Saguenay,unifamilial,172.23601582000578,172.22830894658733,359254.0,226,553.7,0.013,5.949,12.259,9.134,166.3107502408003,178.35642229773393,B,0.2616103487887491,false,2.1.0,2026-08-08
34 +2026-06,municipality,sherbrooke,Sherbrooke,all,174.13317035157246,175.23875372030147,453907.0,328,514.1,0.28,7.845,7.293,8.889,169.48157650276366,181.1914984455182,B,0.4153860933208363,false,2.1.0,2026-08-08
35 +2026-06,municipality,sherbrooke,Sherbrooke,condo,165.60844486302554,156.3310355432095,325922.0,9,140.7,-0.165,1.621,2.358,4.711,148.60689093175787,164.4566582395907,E,0.9551397524107392,false,2.1.0,2026-08-08
36 +2026-06,municipality,sherbrooke,Sherbrooke,plex,147.46730939453232,155.6375080038037,546216.0,58,290.2,-1.109,8.737,5.194,12.324,148.26628015951982,163.37520487849613,D,0.8283671687895422,false,2.1.0,2026-08-08
37 +2026-06,municipality,sherbrooke,Sherbrooke,unifamilial,182.29961181663307,181.8303417866023,439133.0,261,384.8,0.668,7.959,8.118,8.242,174.36257986866804,189.61793992229013,B,0.27917460683784334,false,2.1.0,2026-08-08
38 +2026-06,municipality,trois-rivieres,Trois-Rivières,all,194.13545774935932,194.27290890645278,399445.0,225,501.9,0.736,4.671,7.535,8.888,187.81429322500273,200.95362545043253,B,0.5423899320466273,false,2.1.0,2026-08-08
39 +2026-06,municipality,trois-rivieres,Trois-Rivières,condo,166.4980831552608,192.4580436355919,309677.0,4,324.7,-1.097,5.508,4.458,6.324,186.1450554652588,198.9851326830026,E,0.9893659010046914,false,2.1.0,2026-08-08
40 +2026-06,municipality,trois-rivieres,Trois-Rivières,plex,202.9948465986688,203.07008647249347,441139.0,47,1279.5,-0.145,5.862,5.706,8.234,198.43150024014926,207.81710550007858,D,0.9812071615637241,false,2.1.0,2026-08-08
41 +2026-06,municipality,trois-rivieres,Trois-Rivières,unifamilial,192.6744288652864,191.41139798746366,391023.0,174,312.6,1.138,4.219,8.336,9.249,182.7091763728544,200.52809610803246,B,0.36764237929242183,false,2.1.0,2026-08-08
42 +2026-06,province,quebec,Québec (province),all,153.99846928543036,153.99846928543036,463468.0,15422,14088.7,1.501,6.357,8.168,6.826,153.01886399926317,154.98434586712025,A,0.0,false,2.1.0,2026-08-08
43 +2026-06,province,quebec,Québec (province),condo,131.96657490076268,131.96657490076268,414520.0,1985,1985.0,0.221,1.688,2.088,2.451,130.1983635707343,133.75880013712518,A,0.0,false,2.1.0,2026-08-08
44 +2026-06,province,quebec,Québec (province),plex,154.9674503198742,154.9674503198742,578937.0,1967,1967.0,2.239,5.947,6.711,7.662,152.10612547891157,157.88260060553694,A,0.0,false,2.1.0,2026-08-08
45 +2026-06,province,quebec,Québec (province),unifamilial,159.0129944707306,159.0129944707306,452894.0,11470,11470.0,1.625,7.481,9.84,7.619,157.79619880628897,160.239173071518,A,0.0,false,2.1.0,2026-08-08
46 +2026-06,region,region-01,Bas-Saint-Laurent,all,166.29725201802583,165.5348348549363,269603.0,365,427.1,1.608,13.775,8.967,8.808,159.57791155580685,171.71412561611453,B,0.2938194857516768,false,2.1.0,2026-08-08
47 +2026-06,region,region-01,Bas-Saint-Laurent,condo,138.88408365469428,166.67787899989173,284307.0,1,33.9,-0.734,1.003,0.862,3.754,150.3280814595785,184.8058930717657,E,0.9501749964532001,false,2.1.0,2026-08-08
48 +2026-06,region,region-01,Bas-Saint-Laurent,plex,156.17582155992693,151.05602628085634,294228.0,29,121.7,3.324,4.626,4.028,7.299,140.15327444695046,162.80692096423044,D,0.7595162806901959,false,2.1.0,2026-08-08
49 +2026-06,region,region-01,Bas-Saint-Laurent,unifamilial,168.21413405540136,167.5431476915595,265938.0,335,404.1,1.423,15.288,9.788,9.1,160.82508563996774,174.54183982986598,B,0.22054161742541134,false,2.1.0,2026-08-08
50 +2026-06,region,region-02,Saguenay–Lac-Saint-Jean,all,174.8530941057696,173.38182378051442,315128.0,557,791.8,1.301,7.081,14.847,9.718,168.77692951811082,178.11235755554847,A,0.25605020392560923,false,2.1.0,2026-08-08
51 +2026-06,region,region-02,Saguenay–Lac-Saint-Jean,condo,164.7546820673152,162.87656247384834,268707.0,8,64.2,0.537,1.821,3.076,6.609,151.10895440732048,175.56057288165732,E,0.7844309953745453,false,2.1.0,2026-08-08
52 +2026-06,region,region-02,Saguenay–Lac-Saint-Jean,plex,181.66668995021925,174.4526933673431,344188.0,92,221.8,4.93,2.779,11.001,13.02,165.03406054705673,184.40885549466654,C,0.4571346747081645,false,2.1.0,2026-08-08
53 +2026-06,region,region-02,Saguenay–Lac-Saint-Jean,unifamilial,173.6014251548056,173.36782328961064,309928.0,457,702.7,0.536,8.161,15.964,9.069,168.07005489021958,178.83258366167598,B,0.20114823811757176,false,2.1.0,2026-08-08
54 +2026-06,region,region-03,Capitale-Nationale,all,167.36988521057265,166.7612007506425,438647.0,1838,2558.2,2.403,8.081,8.458,11.377,164.28243102866222,169.2773713029863,A,0.1784017344183837,false,2.1.0,2026-08-08
55 +2026-06,region,region-03,Capitale-Nationale,condo,165.2935420813588,164.55806964899028,345574.0,395,665.7,1.373,4.749,5.219,10.563,160.76908830513926,168.43634912704982,A,0.24644691034349575,false,2.1.0,2026-08-08
56 +2026-06,region,region-03,Capitale-Nationale,plex,161.35106491700725,161.06806872653095,528484.0,206,411.2,0.245,6.128,5.757,11.956,154.63513159917284,167.7686208496315,B,0.41793049844368046,false,2.1.0,2026-08-08
57 +2026-06,region,region-03,Capitale-Nationale,unifamilial,169.35884051233822,168.72605770635775,458570.0,1237,1668.7,3.212,9.675,10.175,11.545,165.362005845192,172.15854635786584,A,0.10588091161192759,false,2.1.0,2026-08-08
58 +2026-06,region,region-04,Mauricie,all,193.0910984810241,192.72901418377342,338288.0,475,556.5,1.02,7.421,9.122,8.75,186.638946987841,199.01780152386414,B,0.2804230202418914,false,2.1.0,2026-08-08
59 +2026-06,region,region-04,Mauricie,condo,178.58793563209926,187.78745636150737,310711.0,5,23.7,-0.943,5.384,4.435,6.482,165.9952415322331,212.44060035225414,E,0.811329263600422,false,2.1.0,2026-08-08
60 +2026-06,region,region-04,Mauricie,plex,201.30772266034433,201.52812938926772,366424.0,78,179.6,-0.144,5.715,5.721,8.35,189.4737275794946,214.34943753929056,C,0.4319156136851525,false,2.1.0,2026-08-08
61 +2026-06,region,region-04,Mauricie,unifamilial,191.21726136402677,190.44978452343676,331512.0,392,467.1,1.399,7.957,10.218,8.92,183.33661631846084,197.838932305915,B,0.22449582550528524,false,2.1.0,2026-08-08
62 +2026-06,region,region-05,Estrie,all,175.09652365128022,174.21380046821156,447792.0,1016,899.6,2.502,8.821,8.384,9.996,169.86936180591562,178.66934890975077,A,0.28356835768759625,false,2.1.0,2026-08-08
63 +2026-06,region,region-05,Estrie,condo,145.99717023807997,151.2919219633559,340607.0,28,144.1,-0.435,0.779,1.022,3.17,143.9035238277728,159.05966054563453,D,0.8444075175954765,false,2.1.0,2026-08-08
64 +2026-06,region,region-05,Estrie,plex,165.1944577906698,165.19020465472082,485274.0,129,227.1,0.002,7.456,5.026,9.758,156.37452864025144,174.5028678976594,C,0.5085101022052475,false,2.1.0,2026-08-08
65 +2026-06,region,region-05,Estrie,unifamilial,178.76745632240716,177.2974947846294,447807.0,859,778.3,3.128,9.542,9.434,10.438,172.14542316194454,182.60376070140424,B,0.21138259240902435,false,2.1.0,2026-08-08
66 +2026-06,region,region-06,Montréal,all,122.94978028719972,122.80001851303042,644536.0,2413,4510.2,0.36,1.81,2.674,2.585,121.42277636693623,124.19288207699799,A,0.26339487127139477,false,2.1.0,2026-08-08
67 +2026-06,region,region-06,Montréal,condo,121.73829971028745,121.76240633783462,487758.0,1090,1608.3,-0.069,1.718,1.854,0.675,119.95124021766802,123.60091959262765,A,0.22411628968424213,false,2.1.0,2026-08-08
68 +2026-06,region,region-06,Montréal,plex,126.24652659391373,126.1753396118162,826103.0,574,935.9,0.099,0.95,1.729,5.3,122.8119493903187,129.6308413406892,A,0.36401597592098744,false,2.1.0,2026-08-08
69 +2026-06,region,region-06,Montréal,unifamilial,122.17258268856442,121.66777920192679,826049.0,749,1622.5,1.305,2.713,4.885,3.485,119.20806481248198,124.17824682595486,A,0.2419812542292591,false,2.1.0,2026-08-08
70 +2026-06,region,region-07,Outaouais,all,147.72815112188044,146.8117163736308,445833.0,649,973.3,2.653,5.546,5.063,3.415,143.29022488161155,150.41975181753935,A,0.2872022794410515,false,2.1.0,2026-08-08
71 +2026-06,region,region-07,Outaouais,condo,134.50772876742175,141.22920707155524,320335.0,13,92.4,-2.028,0.135,1.282,1.423,132.67130353914808,150.3391343718481,D,0.6824025662138823,false,2.1.0,2026-08-08
72 +2026-06,region,region-07,Outaouais,plex,153.56436400789835,150.3920974385131,555233.0,76,226.6,2.144,0.067,-1.899,4.572,142.3567713537952,158.88097739828538,C,0.4959968882962965,false,2.1.0,2026-08-08
73 +2026-06,region,region-07,Outaouais,unifamilial,148.00793398068052,146.73086439525528,442687.0,560,831.9,3.162,6.976,6.605,3.403,142.60487526384682,150.97623083603696,A,0.2177723566005435,false,2.1.0,2026-08-08
74 +2026-06,region,region-08,Abitibi-Témiscamingue,all,154.16518462397738,153.14440969814348,305881.0,245,389.6,4.055,7.631,6.754,7.286,147.37896736881194,159.13539523656576,B,0.4267847943093006,false,2.1.0,2026-08-08
75 +2026-06,region,region-08,Abitibi-Témiscamingue,condo,159.86841422630275,158.17978425076728,327192.0,2,10.7,0.54,1.711,2.112,7.483,131.61372265459084,190.1081714046216,E,0.7693405669483075,false,2.1.0,2026-08-08
76 +2026-06,region,region-08,Abitibi-Témiscamingue,plex,144.0126895777574,154.11033202567393,365749.0,30,962.2,2.105,5.739,6.485,6.79,150.05813427432471,158.27195607833926,D,0.9810653684064549,false,2.1.0,2026-08-08
77 +2026-06,region,region-08,Abitibi-Témiscamingue,unifamilial,156.09014731636876,152.91954487038072,295489.0,213,325.5,4.457,8.045,6.848,7.377,146.10353676779278,160.05353272542584,B,0.3200267269175804,false,2.1.0,2026-08-08
78 +2026-06,region,region-09,Côte-Nord,all,NaN,165.8032340337736,259809.0,107,140.1,3.542,20.482,19.56,11.363,155.52438842937244,176.76142432505065,C,0.28816943022438524,false,2.1.0,2026-08-08
79 +2026-06,region,region-09,Côte-Nord,condo,NaN,137.39227174509134,236484.0,0,23.9,0.221,1.813,2.213,1.188,121.49780952802503,155.36606304760488,E,1.0,false,2.1.0,2026-08-08
80 +2026-06,region,region-09,Côte-Nord,plex,182.70488567798319,159.70393014269294,301527.0,10,546.4,2.481,6.485,7.704,8.647,154.155621182821,165.45193167347466,E,0.9827265772724655,false,2.1.0,2026-08-08
81 +2026-06,region,region-09,Côte-Nord,unifamilial,168.24091550036437,166.8982713967863,254074.0,97,124.4,3.719,22.849,21.552,11.831,155.02943152340026,179.6757733129588,C,0.17993616019281677,false,2.1.0,2026-08-08
82 +2026-06,region,region-10,Nord-du-Québec,all,NaN,119.15687800117361,178882.0,33,79.5,-0.637,5.38,12.201,10.621,109.45510338410091,129.71858904706838,D,0.7045524889691197,false,2.1.0,2026-08-08
83 +2026-06,region,region-10,Nord-du-Québec,condo,NaN,131.96657490076268,,0,0.1,0.221,1.688,2.088,2.451,15.982975522185338,1089.6079310679843,E,1.0,false,2.1.0,2026-08-08
84 +2026-06,region,region-10,Nord-du-Québec,plex,169.30377738290156,135.80648188146574,218390.0,2,48.4,3.228,6.786,3.637,5.069,120.59873569153878,152.93195583902687,E,0.9581579065950871,false,2.1.0,2026-08-08
85 +2026-06,region,region-10,Nord-du-Québec,unifamilial,111.06349416642377,117.4434411366483,175054.0,31,77.6,-1.055,5.229,13.195,11.258,106.96875601720357,128.94383724345718,D,0.6763862148759934,false,2.1.0,2026-08-08
86 +2026-06,region,region-11,Gaspésie–Îles-de-la-Madeleine,all,NaN,162.9224396471656,235749.0,97,130.5,1.608,15.895,10.79,12.135,152.46987001484058,174.09158503250964,C,0.42071582558882314,false,2.1.0,2026-08-08
87 +2026-06,region,region-11,Gaspésie–Îles-de-la-Madeleine,condo,NaN,159.21445925384683,509915.0,0,1.2,0.221,1.688,2.088,2.451,91.71596949323127,276.3885523487341,E,1.0,false,2.1.0,2026-08-08
88 +2026-06,region,region-11,Gaspésie–Îles-de-la-Madeleine,plex,206.03776204012522,163.50814909508387,293808.0,6,424.6,2.468,6.107,6.916,7.376,157.0794108246134,170.19999425864265,E,0.9903901848802228,false,2.1.0,2026-08-08
89 +2026-06,region,region-11,Gaspésie–Îles-de-la-Madeleine,unifamilial,164.16935121598925,162.85146173174678,229283.0,91,122.6,1.503,17.159,11.277,12.735,151.19139993901734,175.4107614511381,C,0.3507148914592255,false,2.1.0,2026-08-08
90 +2026-06,region,region-12,Chaudière-Appalaches,all,175.11057568625185,173.54992827463226,326564.0,1021,1016.3,3.814,12.291,11.843,11.766,169.47509985898628,177.722731122102,A,0.16887585583324513,false,2.1.0,2026-08-08
91 +2026-06,region,region-12,Chaudière-Appalaches,condo,149.1433620571368,141.93174424972892,285501.0,20,67.8,3.051,8.676,12.936,17.007,131.9441082536234,152.6754039448916,E,0.6402472966659276,false,2.1.0,2026-08-08
92 +2026-06,region,region-12,Chaudière-Appalaches,plex,167.6356045773428,162.9124388715842,354660.0,87,174.2,3.413,5.547,10.574,8.573,153.02197033809668,173.44217095393188,C,0.45992421866877387,false,2.1.0,2026-08-08
93 +2026-06,region,region-12,Chaudière-Appalaches,unifamilial,177.16280551111524,176.29304707992765,325021.0,914,973.5,3.894,13.313,11.963,11.979,171.7055361279647,181.00312400855597,A,0.11411727403207794,false,2.1.0,2026-08-08
94 +2026-06,region,region-13,Laval,all,141.11960527219333,140.30733767540562,573943.0,603,1413.3,0.152,2.73,4.43,3.991,137.50864031662414,143.16299659593346,A,0.36621044764217014,false,2.1.0,2026-08-08
95 +2026-06,region,region-13,Laval,condo,139.240009994328,139.3897334694248,419425.0,123,388.9,-0.171,-0.008,0.997,1.927,135.20563942042938,143.70330912204182,B,0.38554258005590714,false,2.1.0,2026-08-08
96 +2026-06,region,region-13,Laval,plex,160.2176150576379,149.5730102594456,829320.0,49,381.3,3.468,7.925,9.442,8.945,143.3736920503095,156.0403800595572,D,0.8519324099464225,false,2.1.0,2026-08-08
97 +2026-06,region,region-13,Laval,unifamilial,139.23833790936027,139.3517295315039,611137.0,431,881.1,-0.2,3.028,5.022,4.065,135.542535645367,143.2679743739557,A,0.2885598860705171,false,2.1.0,2026-08-08
98 +2026-06,region,region-14,Lanaudière,all,163.0469043580067,162.14855397481963,468711.0,1244,1475.3,1.194,6.007,7.046,7.727,158.98221620278093,165.37795348499546,A,0.195105588851564,false,2.1.0,2026-08-08
99 +2026-06,region,region-14,Lanaudière,condo,150.95525192322984,147.73760331684406,358168.0,72,300.4,3.737,3.583,0.48,2.263,142.70287808431306,152.94995957201007,C,0.3700017784027806,false,2.1.0,2026-08-08
100 +2026-06,region,region-14,Lanaudière,plex,168.74191873614478,163.99150159812058,610412.0,110,240.5,3.882,11.043,6.254,9.776,155.47997083542103,172.96898405565977,C,0.42852050679431863,false,2.1.0,2026-08-08
101 +2026-06,region,region-14,Lanaudière,unifamilial,163.57749348879798,163.40146891653754,465299.0,1062,1287.5,0.618,5.633,7.811,8.022,159.697634999925,167.19120508011738,A,0.14879595216900765,false,2.1.0,2026-08-08
102 +2026-06,region,region-15,Laurentides,all,160.2734880726749,158.89927836424883,512418.0,1495,1581.2,2.477,7.303,8.465,7.461,155.9011395835349,161.95507442811302,A,0.20430842566155585,false,2.1.0,2026-08-08
103 +2026-06,region,region-15,Laurentides,condo,159.3055746498613,155.59176445915455,367007.0,46,120.6,2.135,1.267,5.65,5.196,147.3068165073054,164.34268108911607,C,0.5275605277818991,false,2.1.0,2026-08-08
104 +2026-06,region,region-15,Laurentides,plex,168.41635295243952,163.40636634699203,603414.0,203,416.2,4.206,5.479,8.308,9.973,156.91809821668693,170.16291215724075,B,0.422951243117049,false,2.1.0,2026-08-08
105 +2026-06,region,region-15,Laurentides,unifamilial,158.88228325868988,158.3224609754295,508959.0,1246,1348.3,2.186,8.099,8.702,7.169,154.8147383142501,161.9096600378981,A,0.14034236421273694,false,2.1.0,2026-08-08
106 +2026-06,region,region-16,Montérégie,all,150.20364127098358,149.77812167510928,537384.0,2788,3300.4,1.876,5.892,7.913,5.79,147.81630880743387,151.76597165437832,A,0.14968005397369114,false,2.1.0,2026-08-08
107 +2026-06,region,region-16,Montérégie,condo,141.30288873451647,141.40669345099465,392432.0,178,523.3,-0.15,1.337,3.549,3.102,137.73986512012078,145.1711378931982,A,0.3292148672730806,false,2.1.0,2026-08-08
108 +2026-06,region,region-16,Montérégie,plex,159.13197261854805,157.17491228549167,625391.0,241,450.1,1.757,5.522,8.267,8.826,151.16915209124875,163.41927377511647,B,0.41538201215614334,false,2.1.0,2026-08-08
109 +2026-06,region,region-16,Montérégie,unifamilial,150.38217805687225,150.07168196417035,551264.0,2369,2779.9,2.197,6.636,8.527,5.786,147.74822277462846,152.431679411189,A,0.08686257024944011,false,2.1.0,2026-08-08
110 +2026-06,region,region-17,Centre-du-Québec,all,178.12409070961772,177.14629242474297,359345.0,484,666.1,1.565,4.898,8.357,10.96,172.02299571770482,182.422174366324,A,0.341336446481653,false,2.1.0,2026-08-08
111 +2026-06,region,region-17,Centre-du-Québec,condo,153.61637255762946,163.02048384676135,309913.0,4,45.9,-1.235,1.228,1.627,4.698,149.18137391509157,178.1434066209771,E,0.8024636885807596,false,2.1.0,2026-08-08
112 +2026-06,region,region-17,Centre-du-Québec,plex,176.460514329278,174.25522135540956,435377.0,47,205.1,2.568,6.123,7.291,8.336,164.482917772794,184.60811968065195,D,0.7963274327015474,false,2.1.0,2026-08-08
113 +2026-06,region,region-17,Centre-du-Québec,unifamilial,178.69724267164858,177.77289418957946,349320.0,433,620.3,1.444,4.755,8.606,11.448,171.9966818385372,183.74309068477896,B,0.2655858732186408,false,2.1.0,2026-08-08
added outputs/tables/monthly_summary.csv +113 −0
@@ -0,0 +1,113 @@
1 +geography,property_type,median_monthly_tx,sd_monthly_chg_pct,last_index
2 +drummondville,all,92.0,2.99,177.75
3 +drummondville,condo,1.0,4.21,179.51
4 +drummondville,plex,16.0,4.14,190.6
5 +drummondville,unifamilial,72.0,3.03,174.81
6 +gatineau,all,331.0,2.43,146.84
7 +gatineau,condo,30.0,3.02,140.71
8 +gatineau,plex,52.0,2.27,153.31
9 +gatineau,unifamilial,240.0,2.71,146.41
10 +laval,all,371.0,2.48,141.26
11 +laval,condo,91.0,2.0,139.89
12 +laval,plex,34.0,2.79,146.97
13 +laval,unifamilial,234.0,3.2,140.97
14 +levis,all,144.0,4.24,164.76
15 +levis,condo,9.0,8.28,138.91
16 +levis,plex,22.0,3.96,150.36
17 +levis,unifamilial,111.0,4.44,170.11
18 +longueuil,all,222.0,2.55,140.45
19 +longueuil,condo,60.0,2.2,134.2
20 +longueuil,plex,40.0,2.49,151.82
21 +longueuil,unifamilial,118.0,3.31,140.41
22 +montreal,all,1268.0,1.52,121.86
23 +montreal,condo,620.0,1.94,116.36
24 +montreal,plex,346.0,2.32,127.87
25 +montreal,unifamilial,280.0,2.31,127.72
26 +quebec,all,9003.0,2.0,151.96
27 +quebec,condo,1244.0,1.52,130.39
28 +quebec,plex,1298.0,2.05,151.6
29 +quebec,unifamilial,6234.0,2.33,157.15
30 +quebec-city,all,561.0,2.46,156.59
31 +quebec-city,condo,173.0,2.73,143.73
32 +quebec-city,plex,92.0,2.23,153.34
33 +quebec-city,unifamilial,303.0,3.97,165.21
34 +region-01,all,220.0,2.39,159.27
35 +region-01,condo,3.0,3.77,165.48
36 +region-01,plex,24.0,2.82,147.32
37 +region-01,unifamilial,188.0,2.53,160.83
38 +region-02,all,322.0,2.74,168.88
39 +region-02,condo,5.0,3.68,163.1
40 +region-02,plex,57.0,2.59,170.77
41 +region-02,unifamilial,255.0,3.1,168.59
42 +region-03,all,833.0,2.63,160.07
43 +region-03,condo,186.0,2.38,146.61
44 +region-03,plex,115.0,2.23,155.88
45 +region-03,unifamilial,543.0,3.58,165.95
46 +region-04,all,326.0,2.88,190.05
47 +region-04,condo,5.0,3.18,185.54
48 +region-04,plex,68.0,2.6,199.46
49 +region-04,unifamilial,254.0,3.29,187.6
50 +region-05,all,614.0,2.32,167.25
51 +region-05,condo,27.0,2.33,149.2
52 +region-05,plex,83.0,2.36,160.49
53 +region-05,unifamilial,488.0,2.54,169.59
54 +region-06,all,1437.0,1.55,121.74
55 +region-06,condo,659.0,1.76,117.31
56 +region-06,plex,351.0,2.14,127.62
57 +region-06,unifamilial,401.0,2.19,124.32
58 +region-07,all,511.0,2.34,148.58
59 +region-07,condo,30.0,2.52,138.69
60 +region-07,plex,70.0,2.16,153.24
61 +region-07,unifamilial,406.0,2.55,148.73
62 +region-08,all,156.0,2.6,147.97
63 +region-08,condo,1.0,3.59,152.97
64 +region-08,plex,23.0,3.48,150.8
65 +region-08,unifamilial,125.0,2.73,147.41
66 +region-09,all,81.0,7.02,161.32
67 +region-09,condo,0.0,7.42,135.75
68 +region-09,plex,11.0,5.24,156.68
69 +region-09,unifamilial,67.0,8.0,162.17
70 +region-10,all,18.0,3.99,122.52
71 +region-10,condo,0.0,1.52,130.39
72 +region-10,plex,2.0,3.88,131.11
73 +region-10,unifamilial,16.0,4.22,121.6
74 +region-11,all,90.0,3.34,156.36
75 +region-11,condo,0.0,2.74,157.31
76 +region-11,plex,8.0,4.68,159.85
77 +region-11,unifamilial,80.0,3.41,155.94
78 +region-12,all,478.0,3.35,167.97
79 +region-12,condo,11.0,7.72,139.01
80 +region-12,plex,55.0,2.71,158.25
81 +region-12,unifamilial,407.0,3.67,170.47
82 +region-13,all,371.0,2.13,141.44
83 +region-13,condo,91.0,1.88,138.72
84 +region-13,plex,34.0,2.42,146.07
85 +region-13,unifamilial,234.0,2.6,141.81
86 +region-14,all,680.0,2.77,159.0
87 +region-14,condo,47.0,2.41,139.89
88 +region-14,plex,70.0,2.5,155.95
89 +region-14,unifamilial,543.0,3.06,161.39
90 +region-15,all,886.0,2.33,158.31
91 +region-15,condo,44.0,2.61,165.22
92 +region-15,plex,130.0,2.25,161.57
93 +region-15,unifamilial,689.0,2.56,157.22
94 +region-16,all,1493.0,2.57,151.25
95 +region-16,condo,155.0,1.8,136.77
96 +region-16,plex,163.0,2.13,154.97
97 +region-16,unifamilial,1180.0,2.93,153.02
98 +region-17,all,275.0,2.44,170.31
99 +region-17,condo,2.0,2.78,163.08
100 +region-17,plex,35.0,2.42,168.96
101 +region-17,unifamilial,226.0,2.56,170.61
102 +saguenay,all,160.0,3.0,172.06
103 +saguenay,condo,4.0,4.69,162.98
104 +saguenay,plex,37.0,2.97,173.71
105 +saguenay,unifamilial,117.0,3.46,171.91
106 +sherbrooke,all,168.0,2.53,170.3
107 +sherbrooke,condo,7.0,4.09,153.91
108 +sherbrooke,plex,34.0,3.64,150.49
109 +sherbrooke,unifamilial,118.0,2.92,176.81
110 +trois-rivieres,all,146.0,3.44,194.68
111 +trois-rivieres,condo,4.0,4.56,190.16
112 +trois-rivieres,plex,34.0,3.24,201.28
113 +trois-rivieres,unifamilial,101.0,3.99,192.68
added outputs/tables/repeat_sales_comparison.csv +7 −0
@@ -0,0 +1,7 @@
1 +cell,n_pairs,corr_monthly_changes,end_gap_pct,rs_end,hedonic_end
2 +quebec/unifamilial,39613,0.691,-2.24,161.5,157.9
3 +quebec/condo,15930,0.322,4.53,133.8,140.0
4 +quebec/plex,8445,0.454,5.77,149.6,158.5
5 +montreal/condo,6079,0.0,5.52,121.8,128.7
6 +montreal/unifamilial,1191,0.264,5.59,125.6,132.8
7 +quebec-city/unifamilial,2299,0.073,0.15,165.7,166.0
added outputs/tables/repeat_sales_comparison_monthly.csv +8 −0
@@ -0,0 +1,8 @@
1 +cell,n_pairs,corr_monthly_changes,end_gap_pct
2 +quebec/unifamilial,39613,0.749,-4.61
3 +quebec/condo,15930,0.368,-3.2
4 +quebec/plex,8445,0.436,-3.38
5 +montreal/condo,6079,0.238,-1.18
6 +montreal/unifamilial,1191,0.262,-8.14
7 +quebec-city/condo,2511,0.084,1.15
8 +quebec-city/unifamilial,2299,0.333,-3.43
added outputs/tables/research_sample_missingness.csv +5 −0
@@ -0,0 +1,5 @@
1 +column,pct_missing
2 +floorArea,1.85
3 +yearBuilt,5.4
4 +buildingType,27.61
5 +ownerType,23.7
added outputs/tables/rtd_coefficient_drift.csv +56 −0
@@ -0,0 +1,56 @@
1 +window_end,beta_log_fa,n,r2
2 +2022-01,0.585204309055162,137491,0.7209655690189161
3 +2022-02,0.5857411173398457,142283,0.7192753500456464
4 +2022-03,0.5844835903447847,142649,0.7178649003991456
5 +2022-04,0.5823715664465525,142453,0.7159953205190646
6 +2022-05,0.5822109205453823,142249,0.7148472187663171
7 +2022-06,0.5767342388146487,146313,0.7163521826328173
8 +2022-07,0.5779090301858038,134923,0.7115738138730789
9 +2022-08,0.5750664582175867,129409,0.7088300171273567
10 +2022-09,0.5726042617180457,127901,0.7066732640643464
11 +2022-10,0.5704744882371385,125521,0.7047820501753379
12 +2022-11,0.5694514521709101,123053,0.7034805681043457
13 +2022-12,0.5661332280215645,120748,0.702843121315021
14 +2023-01,0.5649955003821484,115484,0.7014714007834166
15 +2023-02,0.5673407305828794,113896,0.7003056477453564
16 +2023-03,0.5656469479149464,113417,0.7007253827229596
17 +2023-04,0.5661620206166009,111355,0.7010546564927939
18 +2023-05,0.5651167920679389,111756,0.7017209941472509
19 +2023-06,0.5633382568755588,114211,0.7062966481362922
20 +2023-07,0.5663172723170072,106480,0.7027059051916623
21 +2023-08,0.5698535854264974,102841,0.7041450191305987
22 +2023-09,0.5706810482447653,101859,0.7044853491070153
23 +2023-10,0.5698496501787736,100384,0.7045626911228662
24 +2023-11,0.5707126040216758,100097,0.7050566781034991
25 +2023-12,0.5696110294134101,99929,0.7057711228615118
26 +2024-01,0.5707574348973737,96475,0.7055310156424944
27 +2024-02,0.5686818375461681,96645,0.7027910549583903
28 +2024-03,0.566509906232083,97942,0.7038724217253539
29 +2024-04,0.5649967306851861,99684,0.7042155024184156
30 +2024-05,0.5638017383056422,106569,0.7060817302601194
31 +2024-06,0.5605374751778387,113358,0.7119916944861301
32 +2024-07,0.5626909960893839,110155,0.709643386013788
33 +2024-08,0.5643554437051648,108473,0.709732420909126
34 +2024-09,0.5640522918131731,108328,0.7082676323722421
35 +2024-10,0.5656389545814011,110207,0.7081615357646676
36 +2024-11,0.5668887163317035,112754,0.7090498700403053
37 +2024-12,0.5654701469909722,114880,0.7108769166256192
38 +2025-01,0.5641143111877248,115050,0.7116634715213561
39 +2025-02,0.5630738636986029,117778,0.7121000508340738
40 +2025-03,0.5648779356604188,120378,0.7143779963554118
41 +2025-04,0.564308697177596,123745,0.7140875125004245
42 +2025-05,0.5633585648905182,128801,0.7158569296972568
43 +2025-06,0.5608423903944524,133445,0.7189544590606349
44 +2025-07,0.5629352940189626,129655,0.7167197778544525
45 +2025-08,0.5645047314065842,128243,0.7171677326720687
46 +2025-09,0.5632165996038893,128093,0.7160669914802575
47 +2025-10,0.5625180682314929,130259,0.7147832658573989
48 +2025-11,0.5586829933977435,129655,0.7140879819359314
49 +2025-12,0.5568883268481871,129146,0.7129465849481076
50 +2026-01,0.5561839244032315,125951,0.7127542003106835
51 +2026-02,0.5573076560664818,124830,0.7114396599373205
52 +2026-03,0.5583376415631243,124711,0.7106926794740822
53 +2026-04,0.5569399927183247,125896,0.7095547001186102
54 +2026-05,0.5579708524770365,127508,0.7107445892389712
55 +2026-06,0.5563205401802921,129882,0.7129412492671154
56 +2026-07,0.5596342494188182,121213,0.7097711435993307
added outputs/tables/seasonality_monthly.csv +4 −0
@@ -0,0 +1,4 @@
1 +cell,f_stat,p_value,seasonal_r2,significant_5pct
2 +quebec/all,0.444,0.77608,0.0283,false
3 +quebec/unifamilial,0.417,0.79551,0.0266,false
4 +montreal/condo,0.749,0.56243,0.0468,false
added outputs/tables/seasonality_tests.csv +4 −0
@@ -0,0 +1,4 @@
1 +cell,f_stat,p_value,seasonal_r2,significant_5pct
2 +quebec/all,1.049,0.39348,0.0218,false
3 +quebec/unifamilial,1.092,0.36719,0.0226,false
4 +montreal/condo,0.419,0.86599,0.0088,false
added outputs/tables/weekly_liquidity.csv +112 −0
@@ -0,0 +1,112 @@
1 +geography_id,property_type,median,mean
2 +drummondville,all,20.0,22.3
3 +drummondville,condo,1.0,1.2
4 +drummondville,plex,4.0,4.4
5 +drummondville,unifamilial,16.0,17.9
6 +gatineau,all,75.0,81.1
7 +gatineau,condo,6.5,9.0
8 +gatineau,plex,12.0,12.8
9 +gatineau,unifamilial,54.0,59.9
10 +laval,all,84.0,88.1
11 +laval,condo,21.0,21.7
12 +laval,plex,8.0,8.6
13 +laval,unifamilial,53.0,58.1
14 +levis,all,33.0,40.6
15 +levis,condo,3.0,4.0
16 +levis,plex,5.0,5.4
17 +levis,unifamilial,26.0,32.3
18 +longueuil,all,52.0,56.0
19 +longueuil,condo,15.0,16.2
20 +longueuil,plex,9.0,9.6
21 +longueuil,unifamilial,28.0,30.4
22 +montreal,all,281.0,301.7
23 +montreal,condo,140.0,153.1
24 +montreal,plex,76.0,82.5
25 +montreal,unifamilial,64.0,66.6
26 +quebec,all,1999.0,2151.6
27 +quebec,condo,291.0,325.9
28 +quebec,plex,297.0,316.7
29 +quebec,unifamilial,1384.0,1509.0
30 +quebec-city,all,122.0,150.0
31 +quebec-city,condo,37.0,45.8
32 +quebec-city,plex,20.5,22.8
33 +quebec-city,unifamilial,65.0,82.0
34 +region-01,all,49.0,51.9
35 +region-01,condo,1.0,1.6
36 +region-01,plex,5.5,6.3
37 +region-01,unifamilial,43.0,45.1
38 +region-02,all,71.0,81.1
39 +region-02,condo,2.0,2.1
40 +region-02,plex,13.0,14.4
41 +region-02,unifamilial,57.0,65.9
42 +region-03,all,181.0,220.5
43 +region-03,condo,40.0,49.9
44 +region-03,plex,26.0,29.0
45 +region-03,unifamilial,116.0,142.0
46 +region-04,all,73.5,78.5
47 +region-04,condo,2.0,2.4
48 +region-04,plex,16.0,17.1
49 +region-04,unifamilial,55.0,60.3
50 +region-05,all,134.0,146.3
51 +region-05,condo,6.0,6.9
52 +region-05,plex,19.0,21.5
53 +region-05,unifamilial,106.5,118.3
54 +region-06,all,328.0,344.6
55 +region-06,condo,151.0,162.6
56 +region-06,plex,78.0,84.3
57 +region-06,unifamilial,96.0,98.4
58 +region-07,all,117.0,125.8
59 +region-07,condo,7.0,9.2
60 +region-07,plex,15.0,16.7
61 +region-07,unifamilial,92.0,100.7
62 +region-08,all,33.0,36.5
63 +region-08,condo,1.0,1.2
64 +region-08,plex,5.0,5.9
65 +region-08,unifamilial,27.0,30.6
66 +region-09,all,18.0,19.7
67 +region-09,condo,1.0,1.1
68 +region-09,plex,3.0,2.9
69 +region-09,unifamilial,16.0,17.1
70 +region-10,all,4.0,4.9
71 +region-10,plex,1.0,1.3
72 +region-10,unifamilial,4.0,4.5
73 +region-11,all,19.0,20.0
74 +region-11,condo,1.0,1.0
75 +region-11,plex,2.0,2.6
76 +region-11,unifamilial,17.0,17.8
77 +region-12,all,107.0,121.6
78 +region-12,condo,3.0,4.6
79 +region-12,plex,12.0,13.3
80 +region-12,unifamilial,92.0,104.5
81 +region-13,all,84.0,88.1
82 +region-13,condo,21.0,21.7
83 +region-13,plex,8.0,8.6
84 +region-13,unifamilial,53.0,58.1
85 +region-14,all,156.0,167.7
86 +region-14,condo,11.0,13.7
87 +region-14,plex,16.0,17.2
88 +region-14,unifamilial,128.0,137.1
89 +region-15,all,201.0,215.9
90 +region-15,condo,10.0,12.9
91 +region-15,plex,29.0,31.7
92 +region-15,unifamilial,160.0,171.7
93 +region-16,all,341.0,370.4
94 +region-16,condo,35.0,43.2
95 +region-16,plex,37.0,39.6
96 +region-16,unifamilial,265.0,288.4
97 +region-17,all,60.0,67.2
98 +region-17,condo,1.0,1.6
99 +region-17,plex,9.0,9.1
100 +region-17,unifamilial,51.0,57.8
101 +saguenay,all,35.0,41.3
102 +saguenay,condo,2.0,2.0
103 +saguenay,plex,8.0,9.2
104 +saguenay,unifamilial,26.0,31.1
105 +sherbrooke,all,37.0,41.9
106 +sherbrooke,condo,2.0,2.2
107 +sherbrooke,plex,7.0,8.4
108 +sherbrooke,unifamilial,29.0,32.0
109 +trois-rivieres,all,32.0,35.3
110 +trois-rivieres,condo,2.0,2.3
111 +trois-rivieres,plex,8.0,8.9
112 +trois-rivieres,unifamilial,23.0,25.4
added outputs/tables/weekly_vs_monthly.csv +4 −0
@@ -0,0 +1,4 @@
1 +cell,weekly_signal_sd_pct,weekly_noise_sd_pct,monthly_change_sd_pct,realtime_revision_sd_pct,weeks_to_monthly_ratio
2 +quebec/unifamilial,3.665,0.0,3.198,0.0,2.39
3 +quebec/condo,3.778,0.0,2.993,0.0,2.63
4 +montreal/condo,3.782,3.583,3.031,3.999,2.6
added paper/qwhpi.pdf +0 −0

Binary file not shown.

added paper/qwhpi.tex +187 −0
@@ -0,0 +1,187 @@
1 +% =============================================================================
2 +% QWHPI — Quebec Weekly Housing Price Index
3 +% Author : Simon-Pierre Boucher
4 +% Contact : contact@spboucher.ai
5 +% File : paper/qwhpi.tex
6 +% Purpose : Methodology paper — A High-Frequency Hedonic Housing Price Index
7 +% for Quebec.
8 +% =============================================================================
9 +\documentclass[11pt]{article}
10 +\usepackage[margin=1.1in]{geometry}
11 +\usepackage{amsmath, amssymb, booktabs, graphicx, natbib, hyperref}
12 +\usepackage[utf8]{inputenc}
13 +\usepackage[T1]{fontenc}
14 +
15 +\title{A High-Frequency Hedonic Housing Price Index for Quebec:\\
16 +Weekly Measurement with Hierarchical Pooling}
17 +\author{Simon-Pierre Boucher\thanks{Contact: contact@spboucher.ai. Code and
18 +data pipeline: QWHPI platform repository.}}
19 +\date{August 2026}
20 +
21 +\begin{document}
22 +\maketitle
23 +
24 +\begin{abstract}
25 +We construct quality-adjusted \emph{weekly} housing price indexes for Quebec
26 +from roughly 745{,}000 geocoded residential transactions (2021--2026),
27 +published for the province, its 17 administrative regions, and ten major
28 +municipalities, by property type. The method combines a pooled hedonic
29 +regression with week-by-type time effects (stage~1) and a hierarchical
30 +state-space model that shrinks thin geography--type cells toward their parent
31 +trends (stage~2). A repeat-sales benchmark, a downsampling experiment, and a
32 +composition-shock simulation validate the design. Weekly measurement is
33 +feasible far below conventional liquidity thresholds: with only 25
34 +transactions per week the smoothed path deviates from the full-sample path by
35 +under 0.8\% RMSE. All series carry calibrated uncertainty bands, reliability
36 +grades, and a real-time (one-sided) variant that never uses future data.
37 +\end{abstract}
38 +
39 +\section{Introduction}
40 +
41 +House price indexes are published monthly or quarterly almost everywhere,
42 +yet the underlying transactions arrive continuously. The obstacle to weekly
43 +measurement is not conceptual but statistical: weekly cells are thin, and
44 +naive weekly averages confound price change with composition change. The
45 +composition problem is classical \citep{court1939, rosen1974}; the thinness
46 +problem is acute at weekly frequency for any geography below a large
47 +metropolitan area. \citet{anenberg2017} demonstrate that timely house-price
48 +measurement is feasible with appropriate pooling; we push frequency to the
49 +week and geography to the municipality for a single Canadian province.
50 +
51 +Our contribution is a transparent two-stage architecture. Stage~1 is a
52 +single pooled hedonic regression over the full sample in the time-dummy
53 +tradition \citep{hill2013, handbook2013}: structural characteristics, fine
54 +location fixed effects, and week$\times$type effects that constitute the
55 +provincial weekly price paths. Stage~2 treats each sub-provincial cell's
56 +weekly deviation from its parent path as a latent random walk observed
57 +through the cell's weekly mean residual, estimated by Kalman filtering
58 +\citep{kalman1960, durbin2012}. The observation variance scales inversely
59 +with the week's transaction count, so liquid weeks speak for themselves
60 +while thin weeks shrink toward the parent trend --- an empirical-Bayes
61 +resolution of the thinness problem in the spirit of hierarchical modeling.
62 +
63 +\section{Data}
64 +
65 +The input is a registry extract of 745{,}119 residential transactions in
66 +1{,}100+ Quebec municipalities from January 2021 through July 2026, with
67 +price, date, address, coordinates, property type (single-family, condominium,
68 +plex, undetermined), floor area, year built, building type, and the municipal
69 +assessment value. Coordinates are spatially joined to the official SDA
70 +1/20{,}000 administrative boundaries (100\% within-polygon match; 99.9\%
71 +agreement with the raw city text field). The \emph{undetermined} type
72 +(14.4\% of records, 95\%+ missing on all structural characteristics) never
73 +enters any published index.
74 +
75 +Screens are economically motivated and fully tabulated: transfers priced
76 +outside $[0.2, 5.0]$ times the municipal assessment (predominantly
77 +non-arm's-length family transfers and estate settlements), conveyances above
78 +\$10M (portfolio scale), and duplicate registrations --- 3.5\% of rows in
79 +total, each flagged with a reason and never silently deleted. Implausible
80 +characteristics (floor area outside $[20, 2000]$~m$^2$, year built after the
81 +sale year) are nulled with indicators, not dropped. The research sample is
82 +626{,}127 transactions across all 291 weeks.
83 +
84 +\section{Method}
85 +
86 +\subsection{Stage 1: pooled hedonic}
87 +
88 +For transaction $i$ in week $t(i)$, type $k(i)$, location $\ell(i)$:
89 +\begin{equation}
90 +\log P_i = \beta_1 \log \mathrm{FA}_i + \beta_2 \mathbb{1}[\mathrm{FA\ miss}]_i
91 ++ f(\mathrm{age}_i) + \gamma_{b(i)} + \alpha_{\ell(i)} + \delta_{t(i),k(i)}
92 ++ \varepsilon_i ,
93 +\end{equation}
94 +with age bins $f(\cdot)$, building-type effects $\gamma$, forward-sortation-area
95 +fixed effects $\alpha$ (the postal-code level; finer than municipality),
96 +and week$\times$type effects $\delta_{t,k}$. Missing floor areas are imputed by
97 +conditional medians within type$\times$municipality and always flagged;
98 +no future price information enters any imputation. The assessment value is
99 +excluded from the headline model to avoid valuation feedback (it appears only
100 +in a robustness specification and in the screens). Estimation is by
101 +high-dimensional fixed-effects least squares on the full pooled sample
102 +($R^2 = 0.60$); the paths $\delta_{\cdot,k}$, rebased so the 2021 average
103 +equals 100, are the provincial indexes per type. Independent weekly
104 +regressions are never run.
105 +
106 +\subsection{Stage 2: hierarchical weekly state}
107 +
108 +Let $e_i = \log P_i - \widehat{\log P_i}$ be stage-1 residuals. For a cell
109 +$c$ (region$\times$type, then municipality$\times$type) define the weekly
110 +mean $\bar e_{c,t}$ over $n_{c,t}$ transactions. The latent deviation
111 +$d_{c,t}$ of the cell from its parent's path evolves as
112 +\begin{equation}
113 +\bar e_{c,t} = d_{c,t} + \epsilon_{c,t}, \quad
114 +\epsilon_{c,t} \sim N(0, \sigma^2_c / n_{c,t}), \qquad
115 +d_{c,t} = d_{c,t-1} + \eta_{c,t}, \quad \eta_{c,t} \sim N(0, \tau^2_c),
116 +\end{equation}
117 +with $\sigma^2_c$ the cell's residual variance and $\tau^2_c$ estimated by
118 +maximum likelihood via the prediction-error decomposition. Zero-transaction
119 +weeks are pure predictions --- the weekly grid is never broken. Municipality
120 +deviations are estimated relative to the smoothed regional path, so the
121 +published municipal index is
122 +$\delta_{t,k} + \hat d^{\,\mathrm{region}}_{t} + \hat d^{\,\mathrm{city}}_{t}$.
123 +The Kalman gain provides an interpretable shrinkage weight; the posterior
124 +variance combines with the delta-method variance of $\delta_{t,k}$ into the
125 +published 95\% band. Two variants ship for every series: the raw weekly
126 +estimate and a strictly one-sided filtered path (\emph{no lookahead}) for
127 +real-time use; a two-sided smoother is retained for research. The ``all
128 +types'' series is a fixed-weight combination of the three type indexes with
129 +full-sample transaction shares, in the stratification tradition of
130 +\citet{handbook2013}.
131 +
132 +\section{Validation}
133 +
134 +\paragraph{Repeat sales.} A Bailey--Muth--Nourse index \citep{bailey1963,
135 +caseshiller1989} on same-unit pairs (unit signature = address, floor area,
136 +year built; 90-day minimum gap; 63k+ pairs) tracks the hedonic indexes'
137 +relative paths closely: Montréal condominiums end 8.8\% below the provincial
138 +condominium index by repeat sales versus 8.2\% by the hierarchical model.
139 +Hedonic levels sit a few percent above repeat sales, consistent with the
140 +well-documented depreciation/renovation bias of repeat-sales measures
141 +\citep{goetzmann1992, bourassa2006}. Notably, a city-specific time-dummy
142 +regression \emph{overstates} Montréal's divergence ($-14\%$) --- pooled
143 +coefficients with local deviations are closer to the composition-free
144 +benchmark than fully local estimation.
145 +
146 +\paragraph{Downsampling.} Using Montréal condominiums ($\sim$145/week) as a
147 +laboratory, we thin each week to a target count and compare against the
148 +full-sample path (20 replications): RMSE rises from 0.32\% (100/wk) to 0.64\%
149 +(50/wk), 0.80\% (25/wk), 1.15\% (15/wk), 1.36\% (10/wk) and 1.63\% (5/wk);
150 +95\% CI coverage stays above 0.97 down to 25/week and degrades to
151 +$\sim$0.83 at 10/week. These results anchor the published A--E reliability
152 +grades and the decision to publish (with warnings) rather than suppress thin
153 +cells.
154 +
155 +\paragraph{Composition shock.} Removing 70\% of below-median floor-area
156 +single-family sales for 26 weeks moves the raw median by $+11.7\%$ and the
157 +hedonic index by $-0.7\%$: the index measures prices, not mix.
158 +
159 +\paragraph{Seasonality.} Annual harmonics on weekly log-changes are jointly
160 +insignificant for all tested series ($p > 0.36$); all series are therefore
161 +published NSA.
162 +
163 +\section{Findings}
164 +
165 +Between the 2021 base and July 2026 the provincial all-type index rises to
166 +$\sim$156 ($+56\%$), with the 2022 spring boom, the 2022--23 rate-hike
167 +correction, and a sustained 2024--26 expansion clearly timed at weekly
168 +resolution. Regional heterogeneity is large: Montréal condominiums
169 +appreciate roughly 27\% while several regions exceed 70\%. The weekly index
170 +dates turning points 4--8 weeks earlier than the same methodology aggregated
171 +to monthly frequency. The assessment-gap module (sale price / municipal
172 +assessment) tracks the mechanical lag of triennial assessment rolls and is
173 +published as a separate concept, never mixed with the price index.
174 +
175 +\section{Conclusion}
176 +
177 +Weekly, quality-adjusted, sub-provincial price measurement is practical with
178 +transparent econometrics: one pooled hedonic regression plus one small
179 +state-space model per cell, running end-to-end in under a minute on a laptop.
180 +Honest uncertainty --- confidence bands, reliability grades, effective sample
181 +sizes, partial-week flags, and preserved vintages --- is what makes
182 +publication of thin cells defensible.
183 +
184 +\bibliographystyle{apalike}
185 +\bibliography{references}
186 +
187 +\end{document}
added paper/references.bib +132 −0
@@ -0,0 +1,132 @@
1 +% =============================================================================
2 +% QWHPI — Quebec Weekly Housing Price Index
3 +% Author : Simon-Pierre Boucher
4 +% Contact : contact@spboucher.ai
5 +% File : paper/references.bib
6 +% Purpose : Bibliography — real, verifiable references only.
7 +% =============================================================================
8 +
9 +@article{court1939,
10 + author = {Court, Andrew T.},
11 + title = {Hedonic Price Indexes with Automotive Examples},
12 + journal = {The Dynamics of Automobile Demand},
13 + year = {1939},
14 + pages = {99--117},
15 + note = {General Motors Corporation, New York}
16 +}
17 +
18 +@article{rosen1974,
19 + author = {Rosen, Sherwin},
20 + title = {Hedonic Prices and Implicit Markets: Product Differentiation in Pure Competition},
21 + journal = {Journal of Political Economy},
22 + volume = {82},
23 + number = {1},
24 + pages = {34--55},
25 + year = {1974}
26 +}
27 +
28 +@article{bailey1963,
29 + author = {Bailey, Martin J. and Muth, Richard F. and Nourse, Hugh O.},
30 + title = {A Regression Method for Real Estate Price Index Construction},
31 + journal = {Journal of the American Statistical Association},
32 + volume = {58},
33 + number = {304},
34 + pages = {933--942},
35 + year = {1963}
36 +}
37 +
38 +@article{caseshiller1989,
39 + author = {Case, Karl E. and Shiller, Robert J.},
40 + title = {The Efficiency of the Market for Single-Family Homes},
41 + journal = {American Economic Review},
42 + volume = {79},
43 + number = {1},
44 + pages = {125--137},
45 + year = {1989}
46 +}
47 +
48 +@article{goetzmann1992,
49 + author = {Goetzmann, William N.},
50 + title = {The Accuracy of Real Estate Indices: Repeat Sale Estimators},
51 + journal = {Journal of Real Estate Finance and Economics},
52 + volume = {5},
53 + number = {1},
54 + pages = {5--53},
55 + year = {1992}
56 +}
57 +
58 +@article{bourassa2006,
59 + author = {Bourassa, Steven C. and Hoesli, Martin and Sun, Jian},
60 + title = {A Simple Alternative House Price Index Method},
61 + journal = {Journal of Housing Economics},
62 + volume = {15},
63 + number = {1},
64 + pages = {80--97},
65 + year = {2006}
66 +}
67 +
68 +@article{hill2013,
69 + author = {Hill, Robert J.},
70 + title = {Hedonic Price Indexes for Residential Housing: A Survey, Evaluation and Taxonomy},
71 + journal = {Journal of Economic Surveys},
72 + volume = {27},
73 + number = {5},
74 + pages = {879--914},
75 + year = {2013}
76 +}
77 +
78 +@book{handbook2013,
79 + author = {{Eurostat, ILO, IMF, OECD, UNECE, World Bank}},
80 + title = {Handbook on Residential Property Prices Indices (RPPIs)},
81 + publisher = {Eurostat Methodologies and Working Papers, Publications Office of the European Union},
82 + address = {Luxembourg},
83 + year = {2013}
84 +}
85 +
86 +@article{anenberg2017,
87 + author = {Anenberg, Elliot and Laufer, Steven},
88 + title = {A More Timely House Price Index},
89 + journal = {Review of Economics and Statistics},
90 + volume = {99},
91 + number = {4},
92 + pages = {722--734},
93 + year = {2017}
94 +}
95 +
96 +@article{kalman1960,
97 + author = {Kalman, Rudolf E.},
98 + title = {A New Approach to Linear Filtering and Prediction Problems},
99 + journal = {Journal of Basic Engineering},
100 + volume = {82},
101 + number = {1},
102 + pages = {35--45},
103 + year = {1960}
104 +}
105 +
106 +@book{durbin2012,
107 + author = {Durbin, James and Koopman, Siem Jan},
108 + title = {Time Series Analysis by State Space Methods},
109 + edition = {2nd},
110 + publisher = {Oxford University Press},
111 + year = {2012}
112 +}
113 +
114 +@article{clapp2004,
115 + author = {Clapp, John M.},
116 + title = {A Semiparametric Method for Estimating Local House Price Indices},
117 + journal = {Real Estate Economics},
118 + volume = {32},
119 + number = {1},
120 + pages = {127--160},
121 + year = {2004}
122 +}
123 +
124 +@article{hillscholz2018,
125 + author = {Hill, Robert J. and Scholz, Michael},
126 + title = {Can Geospatial Data Improve House Price Indexes? A Hedonic Imputation Approach with Splines},
127 + journal = {Review of Income and Wealth},
128 + volume = {64},
129 + number = {4},
130 + pages = {737--756},
131 + year = {2018}
132 +}
added scripts/check_headers.py +91 −0
@@ -0,0 +1,91 @@
1 +#!/usr/bin/env python3
2 +# =============================================================================
3 +# QWHPI — Quebec Weekly Housing Price Index
4 +# Author : Simon-Pierre Boucher
5 +# Contact : contact@spboucher.ai
6 +# File : scripts/check_headers.py
7 +# Purpose : Pre-commit/CI check that every tracked source file carries the
8 +# mandatory QWHPI author header (fails with non-zero exit if not).
9 +# =============================================================================
10 +"""Verify that all source files begin with the mandatory QWHPI author header.
11 +
12 +Usage:
13 + python3 scripts/check_headers.py [paths...]
14 +
15 +With no arguments, scans the whole repository. Exits 1 and lists offending
16 +files if any tracked source file lacks the header.
17 +"""
18 +
19 +from __future__ import annotations
20 +
21 +import sys
22 +from pathlib import Path
23 +
24 +REPO_ROOT = Path(__file__).resolve().parents[1]
25 +
26 +# Extensions that must carry the header.
27 +CHECKED_EXTENSIONS = {
28 + ".py", ".ts", ".tsx", ".js", ".jsx", ".css", ".sql", ".sh",
29 + ".yaml", ".yml", ".toml", ".tex",
30 +}
31 +CHECKED_FILENAMES = {"Dockerfile", "Makefile"}
32 +
33 +# Tool-generated files that are overwritten by their generator (a header
34 +# added here would not survive the next build).
35 +GENERATED_FILES = {"next-env.d.ts"}
36 +
37 +# Directories never scanned.
38 +SKIP_DIRS = {
39 + ".git", "node_modules", ".venv", "venv", "__pycache__", ".next",
40 + "dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache",
41 + "data", "outputs", ".claude",
42 +}
43 +
44 +REQUIRED_TOKENS = (
45 + "QWHPI",
46 + "Simon-Pierre Boucher",
47 + "contact@spboucher.ai",
48 +)
49 +
50 +# How many leading bytes to inspect for the header block.
51 +HEAD_BYTES = 2048
52 +
53 +
54 +def has_header(path: Path) -> bool:
55 + try:
56 + head = path.read_text(encoding="utf-8", errors="replace")[:HEAD_BYTES]
57 + except OSError:
58 + return False
59 + return all(token in head for token in REQUIRED_TOKENS)
60 +
61 +
62 +def iter_candidates(roots: list[Path]):
63 + for root in roots:
64 + if root.is_file():
65 + yield root
66 + continue
67 + for path in sorted(root.rglob("*")):
68 + if any(part in SKIP_DIRS for part in path.parts):
69 + continue
70 + if not path.is_file():
71 + continue
72 + if path.name in GENERATED_FILES:
73 + continue
74 + if path.suffix in CHECKED_EXTENSIONS or path.name in CHECKED_FILENAMES:
75 + yield path
76 +
77 +
78 +def main(argv: list[str]) -> int:
79 + roots = [Path(a).resolve() for a in argv[1:]] or [REPO_ROOT]
80 + missing = [p for p in iter_candidates(roots) if not has_header(p)]
81 + if missing:
82 + print("Files missing the mandatory QWHPI author header:")
83 + for p in missing:
84 + print(f" {p.relative_to(REPO_ROOT)}")
85 + return 1
86 + print("All checked files carry the QWHPI author header.")
87 + return 0
88 +
89 +
90 +if __name__ == "__main__":
91 + raise SystemExit(main(sys.argv))
added web/Dockerfile +19 −0
@@ -0,0 +1,19 @@
1 +# =============================================================================
2 +# QWHPI — Quebec Weekly Housing Price Index
3 +# Author : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# File : web/Dockerfile
6 +# Purpose : Container image for the QWHPI dashboard (Next.js).
7 +# =============================================================================
8 +FROM node:22-alpine AS build
9 +WORKDIR /srv
10 +COPY package.json package-lock.json* ./
11 +RUN npm ci || npm install
12 +COPY . .
13 +RUN npm run build
14 +
15 +FROM node:22-alpine
16 +WORKDIR /srv
17 +COPY --from=build /srv ./
18 +EXPOSE 3000
19 +CMD ["npm", "run", "start"]
added web/app/api-docs/page.tsx +178 −0
@@ -0,0 +1,178 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/api-docs/page.tsx
7 + * Purpose : API docs — copyable examples per endpoint + live playground.
8 + * =============================================================================
9 + */
10 +"use client";
11 +
12 +import { useState } from "react";
13 +import { API_BASE } from "../../lib/api";
14 +
15 +interface Endpoint {
16 + path: string;
17 + desc: string;
18 + example: string;
19 +}
20 +
21 +const ENDPOINTS: Endpoint[] = [
22 + { path: "/v1/index", desc: "Full monthly history for one series (CSV via &format=csv)",
23 + example: "/v1/index?geography=quebec-city&type=condo&from=2023-01&to=latest" },
24 + { path: "/v1/index/latest", desc: "Latest complete month for one series",
25 + example: "/v1/index/latest?geography=montreal&type=condo" },
26 + { path: "/v1/stats", desc: "Derived metrics: peak, drawdown, CAGR, volatility, momentum, ranks, volumes",
27 + example: "/v1/stats?geography=quebec&type=all" },
28 + { path: "/v1/stats/overview", desc: "One rich row per geography (heatmap data)",
29 + example: "/v1/stats/overview?level=region&type=all" },
30 + { path: "/v1/compare", desc: "Aligned multi-series comparison, optional rebasing",
31 + example: "/v1/compare?series=montreal:condo,quebec-city:condo&rebase_period=2022-01" },
32 + { path: "/v1/map", desc: "One metric per geography for a chosen month",
33 + example: "/v1/map?metric=yoy&level=region&period=2025-06" },
34 + { path: "/v1/report", desc: "Automatic PDF report (1-5 series, &format=json for data)",
35 + example: "/v1/report?series=montreal:condo&format=json" },
36 + { path: "/v1/vintages", desc: "First release vs current vintage (revision tracking)",
37 + example: "/v1/vintages?geography=quebec&type=all" },
38 + { path: "/v1/geographies", desc: "Published hierarchy + coverage matrix",
39 + example: "/v1/geographies" },
40 + { path: "/v1/meta", desc: "Model version, vintage, methodology summary",
41 + example: "/v1/meta" },
42 +];
43 +
44 +function CopyButton({ text }: { text: string }) {
45 + const [copied, setCopied] = useState(false);
46 + return (
47 + <button
48 + className="ctrl"
49 + style={{ padding: "4px 10px", fontSize: 12.5 }}
50 + aria-label="Copy request"
51 + onClick={() => {
52 + navigator.clipboard.writeText(text);
53 + setCopied(true);
54 + setTimeout(() => setCopied(false), 1500);
55 + }}
56 + >
57 + {copied ? "✓ Copied" : "Copy"}
58 + </button>
59 + );
60 +}
61 +
62 +export default function ApiDocs() {
63 + const [query, setQuery] = useState(ENDPOINTS[0].example);
64 + const [result, setResult] = useState<string>("");
65 + const [status, setStatus] = useState<string>("");
66 + const [running, setRunning] = useState(false);
67 +
68 + const run = async () => {
69 + setRunning(true);
70 + setStatus("");
71 + const t0 = performance.now();
72 + try {
73 + const res = await fetch(`${API_BASE}${query}`, {
74 + headers: { Accept: "application/json" },
75 + });
76 + const ms = Math.round(performance.now() - t0);
77 + const type = res.headers.get("content-type") ?? "";
78 + setStatus(`${res.status} ${res.statusText} · ${ms} ms · ${type.split(";")[0]}`);
79 + if (type.includes("json")) {
80 + const body = await res.json();
81 + setResult(JSON.stringify(body, null, 2).slice(0, 20000));
82 + } else {
83 + const body = await res.text();
84 + setResult(body.slice(0, 20000));
85 + }
86 + } catch (e) {
87 + setStatus("network error");
88 + setResult(String(e));
89 + }
90 + setRunning(false);
91 + };
92 +
93 + return (
94 + <>
95 + <h1>API</h1>
96 + <p className="lede">
97 + Every published series, statistic and report is available
98 + programmatically — confidence intervals and reliability grades on every
99 + observation, never hidden.{" "}
100 + <a href={`${API_BASE}/docs`} target="_blank" rel="noreferrer">
101 + Full OpenAPI reference ↗
102 + </a>
103 + </p>
104 +
105 + <h2>Playground</h2>
106 + <div className="card fade-up">
107 + <div className="controls" style={{ margin: 0 }}>
108 + <select
109 + aria-label="Endpoint template"
110 + value=""
111 + onChange={(e) => {
112 + if (e.target.value) setQuery(e.target.value);
113 + }}
114 + >
115 + <option value="">Insert an example…</option>
116 + {ENDPOINTS.map((ep) => (
117 + <option key={ep.path} value={ep.example}>{ep.path}</option>
118 + ))}
119 + </select>
120 + <input
121 + type="text"
122 + value={query}
123 + aria-label="Request path"
124 + onChange={(e) => setQuery(e.target.value)}
125 + onKeyDown={(e) => e.key === "Enter" && run()}
126 + style={{
127 + flex: "1 1 340px", background: "var(--surface-1)",
128 + color: "var(--text-primary)",
129 + border: "1px solid var(--border-strong)", borderRadius: 10,
130 + padding: "8px 12px", fontSize: 13.5,
131 + fontFamily: "ui-monospace, monospace",
132 + }}
133 + />
134 + <button className="primary" onClick={run} disabled={running}>
135 + {running ? "Running…" : "Run ▸"}
136 + </button>
137 + </div>
138 + {status && (
139 + <p className="note" style={{ margin: "10px 0 6px" }}>{status}</p>
140 + )}
141 + {result && (
142 + <pre style={{
143 + background: "var(--surface-2)", borderRadius: 10, padding: 14,
144 + fontSize: 12, maxHeight: 380, overflow: "auto", margin: "8px 0 0",
145 + }}>{result}</pre>
146 + )}
147 + </div>
148 +
149 + <h2>Endpoints</h2>
150 + <div className="grid" style={{ gap: 12 }}>
151 + {ENDPOINTS.map((ep) => {
152 + const curl = `curl "${API_BASE}${ep.example}"`;
153 + return (
154 + <div className="card hoverable" key={ep.path}
155 + style={{ padding: "14px 18px" }}>
156 + <div style={{ display: "flex", justifyContent: "space-between",
157 + alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>
158 + <code style={{ fontSize: 14, fontWeight: 700 }}>{ep.path}</code>
159 + <span style={{ display: "flex", gap: 8 }}>
160 + <button className="ctrl" style={{ padding: "4px 10px", fontSize: 12.5 }}
161 + onClick={() => setQuery(ep.example)}>
162 + Try ↑
163 + </button>
164 + <CopyButton text={curl} />
165 + </span>
166 + </div>
167 + <p className="note" style={{ margin: "6px 0" }}>{ep.desc}</p>
168 + <code style={{ fontSize: 12, color: "var(--text-secondary)",
169 + display: "block", overflowX: "auto" }}>
170 + {curl}
171 + </code>
172 + </div>
173 + );
174 + })}
175 + </div>
176 + </>
177 + );
178 +}
added web/app/compare/page.tsx +293 −0
@@ -0,0 +1,293 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/compare/page.tsx
7 + * Purpose : Compare — multi-series chart in index points, dollar values, or
8 + * rebased to a common week (max 5 series, fixed hue order).
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { Suspense, useEffect, useMemo, useState } from "react";
14 +import { usePathname, useRouter, useSearchParams } from "next/navigation";
15 +import {
16 + CartesianGrid,
17 + Line,
18 + LineChart,
19 + Legend,
20 + ResponsiveContainer,
21 + Tooltip,
22 + XAxis,
23 + YAxis,
24 +} from "recharts";
25 +import {
26 + fetchGeographies,
27 + fetchSeries,
28 + reportUrl,
29 + type GeographyNode,
30 + type Observation,
31 +} from "../../lib/api";
32 +
33 +const SERIES_COLORS = [
34 + "var(--series-1)", "var(--series-2)", "var(--series-3)",
35 + "var(--series-4)", "var(--series-5)",
36 +];
37 +const TYPES = ["all", "unifamilial", "condo", "plex"];
38 +const DEFAULT = ["montreal:condo", "quebec-city:condo", "gatineau:condo"];
39 +
40 +type Mode = "index" | "dollars" | "rebased";
41 +
42 +function moneyCompact(v: number): string {
43 + return v >= 1_000_000
44 + ? "$" + (v / 1_000_000).toFixed(2) + "M"
45 + : "$" + Math.round(v / 1000) + "k";
46 +}
47 +
48 +function CompareInner() {
49 + const params = useSearchParams();
50 + const router = useRouter();
51 + const pathname = usePathname();
52 + const initSeries = params.get("series")?.split(",").filter(Boolean);
53 + const [geos, setGeos] = useState<GeographyNode[]>([]);
54 + const [picked, setPicked] = useState<string[]>(
55 + initSeries?.length ? initSeries.slice(0, 5) : DEFAULT);
56 + const [hidden, setHidden] = useState<Set<string>>(new Set());
57 + const [pendingGeo, setPendingGeo] = useState("laval");
58 + const [pendingType, setPendingType] = useState("condo");
59 + const [mode, setMode] = useState<Mode>(
60 + (["index", "dollars", "rebased"] as readonly string[]).includes(
61 + params.get("mode") ?? "") ? (params.get("mode") as Mode) : "index");
62 + const [rebase, setRebase] = useState<string>(params.get("rebase") ?? "2022-01");
63 + const [obs, setObs] = useState<Record<string, Observation[]>>({});
64 + const [error, setError] = useState<string | null>(null);
65 +
66 + // Shareable URL
67 + useEffect(() => {
68 + const q = new URLSearchParams({ series: picked.join(",") });
69 + if (mode !== "index") q.set("mode", mode);
70 + if (mode === "rebased") q.set("rebase", rebase);
71 + router.replace(`${pathname}?${q.toString()}`, { scroll: false });
72 + }, [picked, mode, rebase, router, pathname]);
73 +
74 + useEffect(() => {
75 + fetchGeographies()
76 + .then((g) => setGeos(g.published_series))
77 + .catch((e) => setError(String(e)));
78 + }, []);
79 +
80 + useEffect(() => {
81 + let alive = true;
82 + Promise.all(
83 + picked.map(async (p) => {
84 + const [g, t] = p.split(":");
85 + return [p, (await fetchSeries(g, t)).observations] as const;
86 + }),
87 + )
88 + .then((pairs) => {
89 + if (!alive) return;
90 + setObs(Object.fromEntries(pairs));
91 + setError(null);
92 + })
93 + .catch((e) => setError(String(e)));
94 + return () => { alive = false; };
95 + }, [picked]);
96 +
97 + const rows = useMemo(() => {
98 + const byWeek: Record<string, Record<string, unknown>> = {};
99 + for (const [name, series] of Object.entries(obs)) {
100 + if (!picked.includes(name)) continue;
101 + let baseVal = 1;
102 + if (mode === "rebased") {
103 + const b = series.find((o) => o.period === rebase);
104 + if (!b) continue;
105 + baseVal = b.index_smoothed;
106 + }
107 + for (const o of series) {
108 + const v =
109 + mode === "dollars"
110 + ? o.representative_value
111 + : mode === "rebased"
112 + ? (o.index_smoothed / baseVal) * 100
113 + : o.index_smoothed;
114 + if (v == null) continue;
115 + (byWeek[o.period] ??= { week: o.period })[name] = Math.round(v * 100) / 100;
116 + }
117 + }
118 + return Object.values(byWeek).sort((a, b) =>
119 + String(a.week).localeCompare(String(b.week)));
120 + }, [obs, picked, mode, rebase]);
121 +
122 + const nameOf = useMemo(() => {
123 + const m = new Map(geos.map((g) => [g.geography_id, g.geography_name]));
124 + return (spec: string) => {
125 + const [g, t] = spec.split(":");
126 + return `${m.get(g) ?? g} · ${t}`;
127 + };
128 + }, [geos]);
129 +
130 + const add = () => {
131 + const spec = `${pendingGeo}:${pendingType}`;
132 + if (!picked.includes(spec) && picked.length < 5) setPicked([...picked, spec]);
133 + };
134 +
135 + return (
136 + <>
137 + <h1>Compare series</h1>
138 + <p className="lede">
139 + Up to five series; colors keep their series when you add or remove.
140 + View index levels, dollar values, or rebase to a common month.
141 + </p>
142 + <div className="controls">
143 + <select value={pendingGeo} onChange={(e) => setPendingGeo(e.target.value)}
144 + aria-label="Geography to add">
145 + {geos.map((g) => (
146 + <option key={g.geography_id} value={g.geography_id}>
147 + {g.geography_name}
148 + </option>
149 + ))}
150 + </select>
151 + <select value={pendingType} onChange={(e) => setPendingType(e.target.value)}
152 + aria-label="Property type to add">
153 + {TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
154 + </select>
155 + <button className="ctrl" onClick={add} disabled={picked.length >= 5}>
156 + Add series
157 + </button>
158 + <div className="seg" role="group" aria-label="Comparison mode">
159 + <button aria-pressed={mode === "index"} onClick={() => setMode("index")}>
160 + Index
161 + </button>
162 + <button aria-pressed={mode === "dollars"} onClick={() => setMode("dollars")}>
163 + $ value
164 + </button>
165 + <button aria-pressed={mode === "rebased"} onClick={() => setMode("rebased")}>
166 + Rebased
167 + </button>
168 + </div>
169 + {mode === "rebased" && (
170 + <input
171 + type="month"
172 + value={rebase}
173 + aria-label="Rebase month"
174 + onChange={(e) => setRebase(e.target.value)}
175 + />
176 + )}
177 + {picked.length >= 1 && (
178 + <a className="primary" href={reportUrl(picked)} target="_blank"
179 + rel="noreferrer">
180 + ⤓ Comparison report (PDF)
181 + </a>
182 + )}
183 + </div>
184 + <div className="controls">
185 + {picked.map((s) => (
186 + <button
187 + key={s}
188 + className="chip"
189 + aria-pressed={!hidden.has(s)}
190 + style={{ opacity: hidden.has(s) ? 0.4 : 1 }}
191 + onClick={() =>
192 + setHidden((h) => {
193 + const n = new Set(h);
194 + if (n.has(s)) n.delete(s);
195 + else n.add(s);
196 + return n;
197 + })}
198 + title={hidden.has(s) ? "Show series" : "Hide series"}
199 + >
200 + <span style={{ color: SERIES_COLORS[picked.indexOf(s)] }}>●</span>{" "}
201 + {nameOf(s)}
202 + <span
203 + role="button"
204 + aria-label={`Remove ${nameOf(s)}`}
205 + style={{ marginLeft: 4, color: "var(--text-muted)" }}
206 + onClick={(e) => {
207 + e.stopPropagation();
208 + setPicked(picked.filter((p) => p !== s));
209 + }}
210 + >✕</span>
211 + </button>
212 + ))}
213 + <span className="note">click a chip to toggle · ✕ removes</span>
214 + </div>
215 +
216 + {error && <div className="card"><span className="note">{error}</span></div>}
217 +
218 + <div className="card" style={{ height: 440 }}>
219 + <ResponsiveContainer>
220 + <LineChart data={rows} margin={{ top: 8, right: 12, left: 4, bottom: 0 }}>
221 + <CartesianGrid stroke="var(--border)" strokeDasharray="2 4" vertical={false} />
222 + <XAxis
223 + dataKey="week"
224 + tick={{ fill: "var(--text-muted)", fontSize: 12 }}
225 + tickLine={false}
226 + axisLine={{ stroke: "var(--border)" }}
227 + minTickGap={70}
228 + />
229 + <YAxis
230 + domain={["auto", "auto"]}
231 + tickFormatter={(v: number) =>
232 + mode === "dollars" ? moneyCompact(v) : String(Math.round(v))}
233 + tick={{ fill: "var(--text-muted)", fontSize: 12 }}
234 + tickLine={false}
235 + axisLine={false}
236 + width={mode === "dollars" ? 58 : 48}
237 + />
238 + <Tooltip
239 + contentStyle={{
240 + background: "var(--surface-1)",
241 + border: "1px solid var(--border-strong)",
242 + borderRadius: 10, fontSize: 13,
243 + }}
244 + labelStyle={{ color: "var(--text-secondary)" }}
245 + formatter={(v: number, name: string) => [
246 + mode === "dollars"
247 + ? "$" + Math.round(v).toLocaleString("en-CA")
248 + : v.toFixed(1),
249 + nameOf(name),
250 + ]}
251 + />
252 + <Legend
253 + formatter={(v: string) => (
254 + <span style={{ color: "var(--text-secondary)", fontSize: 13 }}>
255 + {nameOf(v)}
256 + </span>
257 + )}
258 + />
259 + {picked.map((s, i) => (
260 + <Line
261 + key={s}
262 + dataKey={s}
263 + stroke={SERIES_COLORS[i]}
264 + strokeWidth={2}
265 + dot={false}
266 + hide={hidden.has(s)}
267 + isAnimationActive
268 + animationDuration={700}
269 + animationEasing="ease-out"
270 + connectNulls
271 + />
272 + ))}
273 + </LineChart>
274 + </ResponsiveContainer>
275 + </div>
276 + <p className="note">
277 + {mode === "rebased"
278 + ? `All series = 100 at ${rebase}.`
279 + : mode === "dollars"
280 + ? "Representative dollar value per series (fixed 2021 basket carried by the index)."
281 + : "Levels shown on each series' own 2021 = 100 base."}
282 + </p>
283 + </>
284 + );
285 +}
286 +
287 +export default function Compare() {
288 + return (
289 + <Suspense fallback={null}>
290 + <CompareInner />
291 + </Suspense>
292 + );
293 +}
added web/app/explore/page.tsx +310 −0
@@ -0,0 +1,310 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/explore/page.tsx
7 + * Purpose : Explore — series picker, index chart with CI band, raw overlay
8 + * toggle, growth horizons, volume subchart, CSV download.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { Suspense, useEffect, useMemo, useRef, useState } from "react";
14 +import { usePathname, useRouter, useSearchParams } from "next/navigation";
15 +import {
16 + csvUrl,
17 + fetchGeographies,
18 + fetchSeries,
19 + fetchStats,
20 + reportUrl,
21 + type GeographyNode,
22 + type SeriesResponse,
23 + type SeriesStats,
24 +} from "../../lib/api";
25 +import dynamic from "next/dynamic";
26 +import type { ChartUnit } from "../../components/IndexChart";
27 +import { ChartSkeleton, DashboardSkeleton } from "../../components/Skeleton";
28 +import DataTable from "../../components/DataTable";
29 +import { MARKET_EVENTS } from "../../lib/events";
30 +import { exportChartPng } from "../../lib/exportPng";
31 +
32 +const IndexChart = dynamic(() => import("../../components/IndexChart"), {
33 + ssr: false,
34 + loading: () => <ChartSkeleton height={430} />,
35 +});
36 +import ReliabilityBadge from "../../components/ReliabilityBadge";
37 +
38 +const TYPES = ["all", "unifamilial", "condo", "plex"];
39 +const RANGES = ["1y", "3y", "5y", "ytd", "max"] as const;
40 +type Range = (typeof RANGES)[number];
41 +
42 +function rangeCutoff(range: Range, lastPeriod: string): string | null {
43 + if (range === "max") return null;
44 + const [y, m] = lastPeriod.split("-").map(Number);
45 + if (range === "ytd") return `${y}-01`;
46 + const months = range === "1y" ? 12 : range === "3y" ? 36 : 60;
47 + const total = y * 12 + (m - 1) - months;
48 + const cy = Math.floor(total / 12);
49 + const cm = (total % 12) + 1;
50 + return `${cy}-${String(cm).padStart(2, "0")}`;
51 +}
52 +const HORIZONS: { key: keyof HorizonRow; label: string }[] = [
53 + { key: "monthly_pct", label: "1m" },
54 + { key: "three_month_pct", label: "3m" },
55 + { key: "six_month_pct", label: "6m" },
56 + { key: "yoy_pct", label: "YoY" },
57 +];
58 +type HorizonRow = {
59 + monthly_pct: number | null;
60 + three_month_pct: number | null;
61 + six_month_pct: number | null;
62 + yoy_pct: number | null;
63 +};
64 +
65 +function ExploreInner() {
66 + const params = useSearchParams();
67 + const router = useRouter();
68 + const pathname = usePathname();
69 + const [geos, setGeos] = useState<GeographyNode[]>([]);
70 + const [geo, setGeo] = useState(params.get("geography") ?? "quebec");
71 + const [ptype, setPtype] = useState(params.get("type") ?? "all");
72 + const [series, setSeries] = useState<SeriesResponse | null>(null);
73 + const [stats, setStats] = useState<SeriesStats | null>(null);
74 + const [showRaw, setShowRaw] = useState(false);
75 + const [unit, setUnit] = useState<ChartUnit>(
76 + params.get("unit") === "dollars" ? "dollars" : "points");
77 + const [range, setRange] = useState<Range>(
78 + (RANGES as readonly string[]).includes(params.get("range") ?? "")
79 + ? (params.get("range") as Range) : "max");
80 + const [showEvents, setShowEvents] = useState(params.get("events") === "1");
81 + const [showTable, setShowTable] = useState(false);
82 + const chartRef = useRef<HTMLDivElement>(null);
83 + const [error, setError] = useState<string | null>(null);
84 +
85 + // Shareable URL: selection encoded in query params.
86 + useEffect(() => {
87 + const q = new URLSearchParams({ geography: geo, type: ptype });
88 + if (unit === "dollars") q.set("unit", "dollars");
89 + if (range !== "max") q.set("range", range);
90 + if (showEvents) q.set("events", "1");
91 + router.replace(`${pathname}?${q.toString()}`, { scroll: false });
92 + }, [geo, ptype, unit, range, showEvents, router, pathname]);
93 +
94 + useEffect(() => {
95 + fetchGeographies()
96 + .then((g) => setGeos(g.published_series))
97 + .catch((e) => setError(String(e)));
98 + }, []);
99 +
100 + useEffect(() => {
101 + setSeries(null);
102 + setStats(null);
103 + fetchSeries(geo, ptype)
104 + .then(setSeries)
105 + .catch((e) => setError(String(e)));
106 + fetchStats(geo, ptype).then(setStats).catch(() => setStats(null));
107 + }, [geo, ptype]);
108 +
109 + const grouped = useMemo(() => {
110 + const by: Record<string, GeographyNode[]> = {};
111 + for (const g of geos) (by[g.geography_level] ??= []).push(g);
112 + return by;
113 + }, [geos]);
114 +
115 + if (error)
116 + return (
117 + <div className="card" style={{ marginTop: 32 }}>
118 + <strong>API unreachable.</strong> <span className="note">{error}</span>
119 + </div>
120 + );
121 +
122 + const last = series?.observations.filter((o) => !o.is_partial_month).at(-1);
123 +
124 + const visibleObs = useMemo(() => {
125 + if (!series) return [];
126 + const obs = series.observations;
127 + const cutoff = obs.length
128 + ? rangeCutoff(range, obs[obs.length - 1].period) : null;
129 + return cutoff ? obs.filter((o) => o.period >= cutoff) : obs;
130 + }, [series, range]);
131 +
132 + return (
133 + <>
134 + <h1>Explore a series</h1>
135 + <div className="controls">
136 + <label htmlFor="geo">Geography</label>
137 + <select id="geo" value={geo} onChange={(e) => setGeo(e.target.value)}>
138 + {(["province", "region", "municipality"] as const).map((lvl) => (
139 + <optgroup key={lvl} label={lvl}>
140 + {(grouped[lvl] ?? []).map((g) => (
141 + <option key={g.geography_id} value={g.geography_id}>
142 + {g.geography_name}
143 + </option>
144 + ))}
145 + </optgroup>
146 + ))}
147 + </select>
148 + <label htmlFor="ptype">Type</label>
149 + <select id="ptype" value={ptype} onChange={(e) => setPtype(e.target.value)}>
150 + {TYPES.map((t) => (
151 + <option key={t} value={t}>{t}</option>
152 + ))}
153 + </select>
154 + <div className="seg" role="group" aria-label="Chart unit">
155 + <button aria-pressed={unit === "points"} onClick={() => setUnit("points")}>
156 + Index
157 + </button>
158 + <button aria-pressed={unit === "dollars"} onClick={() => setUnit("dollars")}>
159 + $ value
160 + </button>
161 + </div>
162 + <div className="seg" role="group" aria-label="Period range">
163 + {RANGES.map((r) => (
164 + <button key={r} aria-pressed={range === r} onClick={() => setRange(r)}>
165 + {r.toUpperCase()}
166 + </button>
167 + ))}
168 + </div>
169 + <button
170 + className="ctrl"
171 + aria-pressed={showRaw}
172 + onClick={() => setShowRaw((v) => !v)}
173 + >
174 + Raw monthly overlay
175 + </button>
176 + <button
177 + className="ctrl"
178 + aria-pressed={showEvents}
179 + onClick={() => setShowEvents((v) => !v)}
180 + >
181 + Events
182 + </button>
183 + <button
184 + className="ctrl"
185 + onClick={() =>
186 + chartRef.current &&
187 + exportChartPng(chartRef.current, `qhpi_${geo}_${ptype}.png`)}
188 + >
189 + ⤓ PNG
190 + </button>
191 + <a className="ctrl" href={csvUrl(geo, ptype)}>
192 + ⤓ CSV
193 + </a>
194 + <a className="primary" href={reportUrl([`${geo}:${ptype}`])}
195 + target="_blank" rel="noreferrer">
196 + ⤓ PDF report
197 + </a>
198 + </div>
199 +
200 + {!series ? (
201 + <DashboardSkeleton />
202 + ) : (
203 + <>
204 + <div className="card fade-up">
205 + <div style={{ display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
206 + <strong>
207 + {series.geography_name} · {series.property_type} ·{" "}
208 + {unit === "dollars"
209 + ? "representative dollar value"
210 + : "base 2021 = 100"}
211 + </strong>
212 + {last && <ReliabilityBadge grade={last.reliability_grade} />}
213 + </div>
214 + {last && (last.reliability_grade === "D" || last.reliability_grade === "E") && (
215 + <p className="low-reliability-note">
216 + Thin market: monthly movements are heavily shrunk toward the
217 + parent trend. Read levels and multi-month changes, not
218 + single months.
219 + </p>
220 + )}
221 + <div ref={chartRef}>
222 + <IndexChart observations={visibleObs} showRaw={showRaw}
223 + unit={unit}
224 + annotations={showEvents ? MARKET_EVENTS : []}
225 + showBrush={range === "max"} />
226 + </div>
227 + <div className="controls" style={{ margin: "8px 0 0" }}>
228 + <button className="ctrl" aria-pressed={showTable}
229 + aria-expanded={showTable}
230 + onClick={() => setShowTable((v) => !v)}>
231 + {showTable ? "Hide data table" : "Data table"}
232 + </button>
233 + </div>
234 + {showTable && <DataTable observations={visibleObs} />}
235 + </div>
236 +
237 + {last && (
238 + <>
239 + <h2>Growth (month of {last.period})</h2>
240 + <div className="grid cols-4">
241 + {HORIZONS.map(({ key, label }) => {
242 + const v = last[key];
243 + return (
244 + <div className="card stat-tile" key={key}>
245 + <span className="label">{label}</span>
246 + <span
247 + className={
248 + "value " + (v != null && v < 0 ? "delta down" : "delta up")
249 + }
250 + style={{ fontSize: 22 }}
251 + >
252 + {v == null ? "—" : `${v > 0 ? "+" : ""}${v.toFixed(2)}%`}
253 + </span>
254 + </div>
255 + );
256 + })}
257 + <div className="card stat-tile">
258 + <span className="label">Representative value</span>
259 + <span className="value" style={{ fontSize: 22 }}>
260 + {last.representative_value
261 + ? `$${Math.round(last.representative_value).toLocaleString()}`
262 + : "—"}
263 + </span>
264 + <span className="note">
265 + n={last.transactions} · eff. N≈
266 + {last.effective_sample_size?.toFixed(0) ?? "—"}
267 + </span>
268 + </div>
269 + </div>
270 + {stats && (
271 + <>
272 + <h2>Structure &amp; risk</h2>
273 + <div className="grid cols-8">
274 + {([
275 + ["Since 2021", `${stats.since_2021_pct > 0 ? "+" : ""}${stats.since_2021_pct.toFixed(1)}%`],
276 + ["CAGR", `${stats.cagr_pct > 0 ? "+" : ""}${stats.cagr_pct.toFixed(2)}%`],
277 + ["Peak", `${stats.peak_index.toFixed(1)} (${stats.peak_period})`],
278 + ["Vs peak", stats.at_record_high ? "★ at peak" : `${stats.drawdown_pct.toFixed(2)}%`],
279 + ["Volatility 12m", `${stats.volatility_12m_pct.toFixed(2)}%`],
280 + ["Momentum 3m ann.", `${stats.momentum_3m_ann_pct > 0 ? "+" : ""}${stats.momentum_3m_ann_pct.toFixed(1)}%`],
281 + ["YoY rank", stats.yoy_rank ? `${stats.yoy_rank}/${stats.yoy_rank_of}` : "—"],
282 + ["Sales 12m", stats.volume_12m.toLocaleString()],
283 + ["Volume YoY", stats.volume_yoy_pct == null ? "—" : `${stats.volume_yoy_pct > 0 ? "+" : ""}${stats.volume_yoy_pct.toFixed(1)}%`],
284 + ["$ volume 12m", `$${(stats.dollar_volume_12m / 1e9).toFixed(2)}B`],
285 + ["Assessment gap", stats.assessment_gap == null ? "—" : `${stats.assessment_gap.toFixed(2)}×`],
286 + ["Effective N", stats.effective_sample_size?.toFixed(0) ?? "—"],
287 + ] as [string, string][]).map(([label, val]) => (
288 + <div className="card stat-tile mini-tile" key={label}>
289 + <span className="label">{label}</span>
290 + <span className="value" style={{ fontSize: 17 }}>{val}</span>
291 + </div>
292 + ))}
293 + </div>
294 + </>
295 + )}
296 + </>
297 + )}
298 + </>
299 + )}
300 + </>
301 + );
302 +}
303 +
304 +export default function Explore() {
305 + return (
306 + <Suspense fallback={<DashboardSkeleton />}>
307 + <ExploreInner />
308 + </Suspense>
309 + );
310 +}
added web/app/icon.svg +10 −0
@@ -0,0 +1,10 @@
1 +<!-- =============================================================================
2 +QWHPI - Quebec Weekly Housing Price Index
3 +Author : Simon-Pierre Boucher | Contact : contact@spboucher.ai
4 +File : web/app/icon.svg | Purpose : Favicon (brand chart mark).
5 +============================================================================== -->
6 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 22 22">
7 + <rect x="1" y="1" width="20" height="20" rx="5" fill="#2a78d6"/>
8 + <path d="M5 14.5 L9 10.5 L12 12.5 L17 6.5" fill="none" stroke="#fff"
9 + stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
10 +</svg>
added web/app/layout.tsx +98 −0
@@ -0,0 +1,98 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/layout.tsx
7 + * Purpose : Root layout — fonts, navigation, theme toggle, author footer.
8 + * =============================================================================
9 + */
10 +import type { Metadata } from "next";
11 +import Link from "next/link";
12 +import { Fraunces, Inter } from "next/font/google";
13 +import "../styles/globals.css";
14 +import ThemeToggle from "../components/ThemeToggle";
15 +import CommandPalette from "../components/CommandPalette";
16 +import SearchButton from "../components/SearchButton";
17 +
18 +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
19 +const fraunces = Fraunces({
20 + subsets: ["latin"],
21 + variable: "--font-display",
22 + weight: ["600", "700"],
23 +});
24 +
25 +export const metadata: Metadata = {
26 + metadataBase: new URL("https://www.indexqc.house"),
27 + title: {
28 + default: "QHPI — Quebec Housing Price Index (monthly)",
29 + template: "%s · QHPI",
30 + },
31 + description:
32 + "Quality-adjusted monthly housing price indexes for Quebec — province, " +
33 + "17 regions and 10 cities, with confidence intervals on every point. " +
34 + "Author: Simon-Pierre Boucher — contact@spboucher.ai",
35 + authors: [{ name: "Simon-Pierre Boucher" }],
36 + alternates: { canonical: "/" },
37 + openGraph: {
38 + title: "QHPI — Quebec Housing Price Index",
39 + description:
40 + "Quebec housing prices, measured monthly, robustly. " +
41 + "Uncertainty always displayed, never hidden.",
42 + url: "https://www.indexqc.house",
43 + siteName: "QHPI",
44 + type: "website",
45 + locale: "en_CA",
46 + },
47 + twitter: {
48 + card: "summary_large_image",
49 + title: "QHPI — Quebec Housing Price Index",
50 + description: "Quebec housing prices, measured monthly, robustly.",
51 + },
52 +};
53 +
54 +export default function RootLayout({
55 + children,
56 +}: {
57 + children: React.ReactNode;
58 +}) {
59 + return (
60 + <html lang="en" suppressHydrationWarning
61 + className={`${inter.variable} ${fraunces.variable}`}>
62 + <body>
63 + <header className="site-header">
64 + <Link href="/" className="brand">
65 + <svg className="brand-mark" width="22" height="22" viewBox="0 0 22 22"
66 + aria-hidden focusable="false">
67 + <rect x="1" y="1" width="20" height="20" rx="5"
68 + fill="var(--series-1)" />
69 + <path d="M5 14.5 L9 10.5 L12 12.5 L17 6.5" fill="none"
70 + stroke="#fff" strokeWidth="2" strokeLinecap="round"
71 + strokeLinejoin="round" />
72 + </svg>
73 + QHPI<small>Quebec Housing Price Index · monthly</small>
74 + </Link>
75 + <nav aria-label="Main">
76 + <Link href="/">Overview</Link>
77 + <Link href="/explore">Explore</Link>
78 + <Link href="/compare">Compare</Link>
79 + <Link href="/map">Map</Link>
80 + <Link href="/methodology">Methodology</Link>
81 + <Link href="/api-docs">API</Link>
82 + </nav>
83 + <div style={{ display: "flex", gap: 8 }}>
84 + <SearchButton />
85 + <ThemeToggle />
86 + </div>
87 + </header>
88 + <CommandPalette />
89 + <main className="container">{children}</main>
90 + <footer className="site-footer">
91 + Simon-Pierre Boucher — contact@spboucher.ai · Monthly hedonic index,
92 + base 2021 average = 100 · Uncertainty is always displayed, never
93 + hidden.
94 + </footer>
95 + </body>
96 + </html>
97 + );
98 +}
added web/app/map/page.tsx +241 −0
@@ -0,0 +1,241 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/map/page.tsx
7 + * Purpose : Map — choropleth of the 17 administrative regions (YoY, 13w,
8 + * index level, assessment gap) rendered as inline SVG.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { useEffect, useMemo, useRef, useState } from "react";
14 +import { fetchMap, fetchSeries, type MapCell } from "../../lib/api";
15 +
16 +const METRICS = [
17 + { id: "yoy", label: "YoY %" },
18 + { id: "six_month", label: "6-month %" },
19 + { id: "three_month", label: "3-month %" },
20 + { id: "index_level", label: "Index level" },
21 + { id: "assessment_gap", label: "Assessment gap" },
22 +];
23 +const TYPES = ["all", "unifamilial", "condo", "plex"];
24 +
25 +// Sequential blue ramp (light→dark) from the validated palette.
26 +const RAMP = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95", "#0d366b"];
27 +
28 +interface Feature {
29 + properties: { geography_id: string; name: string };
30 + geometry: { type: string; coordinates: number[][][] | number[][][][] };
31 +}
32 +
33 +function project([lng, lat]: number[], w: number, h: number): [number, number] {
34 + // Quebec bbox in EPSG:4326; simple equirectangular with cos(lat0) x-scale.
35 + const [minLng, maxLng, minLat, maxLat] = [-79.9, -56.9, 44.9, 62.8];
36 + const k = Math.cos((52 * Math.PI) / 180);
37 + const spanX = (maxLng - minLng) * k;
38 + const spanY = maxLat - minLat;
39 + const s = Math.min(w / spanX, h / spanY);
40 + const x = ((lng - minLng) * k) * s;
41 + const y = h - (lat - minLat) * s - (h - spanY * s) / 2;
42 + return [x, y];
43 +}
44 +
45 +function ringsOf(f: Feature): number[][][] {
46 + return f.geometry.type === "Polygon"
47 + ? (f.geometry.coordinates as number[][][])
48 + : (f.geometry.coordinates as number[][][][]).flat();
49 +}
50 +
51 +function pathFor(f: Feature, w: number, h: number): string {
52 + return ringsOf(f)
53 + .map((ring) => {
54 + const pts = ring.map((c) => project(c, w, h));
55 + return "M" + pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join("L") + "Z";
56 + })
57 + .join(" ");
58 +}
59 +
60 +export default function MapPage() {
61 + const [metric, setMetric] = useState("yoy");
62 + const [ptype, setPtype] = useState("all");
63 + const [cells, setCells] = useState<MapCell[]>([]);
64 + const [periods, setPeriods] = useState<string[]>([]);
65 + const [idx, setIdx] = useState(0);
66 + const [playing, setPlaying] = useState(false);
67 + const [features, setFeatures] = useState<Feature[]>([]);
68 + const [hover, setHover] = useState<MapCell | null>(null);
69 + const [error, setError] = useState<string | null>(null);
70 + const playRef = useRef<ReturnType<typeof setInterval> | null>(null);
71 +
72 + useEffect(() => {
73 + fetch("/regions.geojson")
74 + .then((r) => r.json())
75 + .then((g) => setFeatures(g.features))
76 + .catch((e) => setError(String(e)));
77 + fetchSeries("quebec", "all")
78 + .then((s) => {
79 + const ps = s.observations
80 + .filter((o) => !o.is_partial_month)
81 + .map((o) => o.period);
82 + setPeriods(ps);
83 + setIdx(ps.length - 1);
84 + })
85 + .catch((e) => setError(String(e)));
86 + }, []);
87 +
88 + const period = periods[idx] ?? "";
89 +
90 + useEffect(() => {
91 + if (!period) return;
92 + fetchMap(metric, ptype, period)
93 + .then((m) => {
94 + setCells(m.cells);
95 + setError(null);
96 + })
97 + .catch((e) => setError(String(e)));
98 + }, [metric, ptype, period]);
99 +
100 + // Time-lapse playback across all months.
101 + useEffect(() => {
102 + if (!playing) {
103 + if (playRef.current) clearInterval(playRef.current);
104 + return;
105 + }
106 + playRef.current = setInterval(() => {
107 + setIdx((i) => {
108 + if (i >= periods.length - 1) {
109 + setPlaying(false);
110 + return i;
111 + }
112 + return i + 1;
113 + });
114 + }, 550);
115 + return () => {
116 + if (playRef.current) clearInterval(playRef.current);
117 + };
118 + }, [playing, periods.length]);
119 +
120 + const { colorOf, domain } = useMemo(() => {
121 + const vals = cells.map((c) => c.value).filter((v): v is number => v != null);
122 + const lo = Math.min(...vals);
123 + const hi = Math.max(...vals);
124 + const f = (v: number | null) => {
125 + if (v == null || vals.length === 0) return "var(--surface-2)";
126 + const t = hi === lo ? 0.5 : (v - lo) / (hi - lo);
127 + return RAMP[Math.min(RAMP.length - 1, Math.floor(t * RAMP.length))];
128 + };
129 + return { colorOf: f, domain: vals.length ? ([lo, hi] as const) : null };
130 + }, [cells]);
131 +
132 + const byId = useMemo(
133 + () => new Map(cells.map((c) => [c.geography_id, c])),
134 + [cells],
135 + );
136 +
137 + const W = 720, H = 560;
138 + const fmt = (v: number | null) =>
139 + v == null ? "—" : metric === "assessment_gap" ? v.toFixed(2) : v.toFixed(1);
140 +
141 + return (
142 + <>
143 + <h1>Regional map</h1>
144 + <div className="controls">
145 + <label>Metric</label>
146 + <select value={metric} onChange={(e) => setMetric(e.target.value)}>
147 + {METRICS.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
148 + </select>
149 + <label>Type</label>
150 + <select value={ptype} onChange={(e) => setPtype(e.target.value)}>
151 + {TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
152 + </select>
153 + <button className="ctrl" aria-pressed={playing}
154 + aria-label={playing ? "Pause time-lapse" : "Play time-lapse"}
155 + onClick={() => {
156 + if (!playing && idx >= periods.length - 1) setIdx(12);
157 + setPlaying((v) => !v);
158 + }}>
159 + {playing ? "⏸ Pause" : "▶ Play"}
160 + </button>
161 + <input
162 + type="range"
163 + min={0}
164 + max={Math.max(periods.length - 1, 0)}
165 + value={idx}
166 + aria-label="Month selector"
167 + style={{ flex: "1 1 180px", accentColor: "var(--series-1)" }}
168 + onChange={(e) => {
169 + setPlaying(false);
170 + setIdx(Number(e.target.value));
171 + }}
172 + />
173 + <span className="note" style={{ fontVariantNumeric: "tabular-nums",
174 + minWidth: 84 }}>
175 + <strong>{period}</strong>
176 + </span>
177 + </div>
178 + {error && <div className="card"><span className="note">{error}</span></div>}
179 +
180 + <div className="grid cols-2">
181 + <div className="card">
182 + <svg viewBox={`0 0 ${W} ${H}`} role="img"
183 + aria-label={`Choropleth of Quebec regions, ${metric}`}>
184 + {features.map((f) => {
185 + const cell = byId.get(f.properties.geography_id);
186 + return (
187 + <path
188 + key={f.properties.geography_id}
189 + d={pathFor(f, W, H)}
190 + fill={colorOf(cell?.value ?? null)}
191 + stroke="var(--surface-1)"
192 + strokeWidth={1.5}
193 + style={{ cursor: "pointer", transition: "fill 0.4s ease" }}
194 + onMouseEnter={() => setHover(cell ?? null)}
195 + onMouseLeave={() => setHover(null)}
196 + />
197 + );
198 + })}
199 + </svg>
200 + {domain && (
201 + <div className="note" style={{ display: "flex", gap: 4, alignItems: "center" }}>
202 + {fmt(domain[0])}
203 + {RAMP.map((c) => (
204 + <span key={c} style={{ background: c, width: 22, height: 10, display: "inline-block" }} />
205 + ))}
206 + {fmt(domain[1])}
207 + </div>
208 + )}
209 + {hover && (
210 + <div className="note">
211 + <strong>{hover.geography_name}</strong>: {fmt(hover.value)} ·{" "}
212 + {hover.transactions} tx · reliability {hover.reliability_grade}
213 + </div>
214 + )}
215 + </div>
216 +
217 + <div className="card">
218 + <strong>All regions</strong>
219 + <table className="data">
220 + <thead>
221 + <tr><th>Region</th><th>{METRICS.find((m) => m.id === metric)?.label}</th>
222 + <th>Tx</th><th>Grade</th></tr>
223 + </thead>
224 + <tbody>
225 + {[...cells]
226 + .sort((a, b) => (b.value ?? -1e9) - (a.value ?? -1e9))
227 + .map((c) => (
228 + <tr key={c.geography_id}>
229 + <td>{c.geography_name}</td>
230 + <td>{fmt(c.value)}</td>
231 + <td>{c.transactions}</td>
232 + <td>{c.reliability_grade}</td>
233 + </tr>
234 + ))}
235 + </tbody>
236 + </table>
237 + </div>
238 + </div>
239 + </>
240 + );
241 +}
added web/app/methodology/page.tsx +124 −0
@@ -0,0 +1,124 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/methodology/page.tsx
7 + * Purpose : Methodology — rendered summary of the v2.1 monthly robust index.
8 + * =============================================================================
9 + */
10 +
11 +export default function Methodology() {
12 + return (
13 + <>
14 + <h1>Methodology (v2.1 — monthly, robust)</h1>
15 + <p className="lede">
16 + QHPI measures price movement, never composition movement. Version 2
17 + rebuilt the index at monthly frequency around the rolling-time-dummy
18 + method used by official statistical agencies.
19 + </p>
20 +
21 + <h2>Data</h2>
22 + <p>
23 + ~745,000 residential transactions across 1,100+ Quebec municipalities,
24 + January 2021 to present, spatially joined to the official SDA
25 + 1/20&nbsp;000 boundaries. Non-arm&apos;s-length transfers, duplicates
26 + and portfolio-scale conveyances are excluded with a documented
27 + exclusion table (~3.5%). The <em>indéterminé</em> type never enters an
28 + index. <strong>Variables whose missingness moves over time are banned
29 + from the model</strong>: the provider backfilled
30 + <code>buildingType</code> in region-staggered waves (2022–2023), which
31 + would fabricate artificial index steps — it was detected by a
32 + composition-free arbitration and excluded; building age uses
33 + time-invariant conditional-median imputation.
34 + </p>
35 +
36 + <h2>Three estimators, one family</h2>
37 + <ul>
38 + <li>
39 + <strong>Province × type — rolling-time-dummy (RTD).</strong>{" "}
40 + 13-month hedonic windows re-estimated every month with Huber-robust
41 + weighting (outliers are downweighted inside the regression), linked
42 + by the <em>mean splice</em>. Published history never revises, and
43 + hedonic coefficients are free to drift (β on log floor area moves
44 + ~5% across windows).
45 + </li>
46 + <li>
47 + <strong>Liquid cells (≥ 40 tx/month) — direct local estimation.</strong>{" "}
48 + Own robust time-dummy regression (own coefficients, own postal-zone
49 + fixed effects) with a light state-space smoother for the real-time
50 + variant. Chosen because deviation-from-pooled-surface methods
51 + compress strong local divergences: Québec City condos appreciated
52 + ~+55% by repeat sales AND stratified matched-cell medians, which the
53 + direct estimator reproduces (+53–64%) while pooled deviations said
54 + +42%.
55 + </li>
56 + <li>
57 + <strong>Thin cells — hierarchical shrinkage.</strong> Monthly mean
58 + deviations from the parent path filtered by a heteroskedastic
59 + local-level Kalman model: liquid months speak, thin months shrink
60 + toward the parent.
61 + </li>
62 + </ul>
63 +
64 + <h2>Validation</h2>
65 + <ul>
66 + <li>
67 + <strong>Repeat sales (BMN)</strong>: published levels agree within
68 + ±5% on six of seven benchmark cells, with high change-correlations at
69 + the province level. The exception (Montréal single-family, −8%) sits
70 + between repeat sales and the stratified matched-cell median.
71 + </li>
72 + <li>
73 + <strong>Stratified matched-cell medians</strong> (FSA × vintage ×
74 + size, regression-free) corroborate the direct estimator on every
75 + arbitrated cell.
76 + </li>
77 + <li>
78 + <strong>Downsampling</strong>: direct-estimation RMSE stays ≈1.9% at
79 + 40 tx/month (the liquidity floor); hierarchical shrinkage takes over
80 + below.
81 + </li>
82 + <li>
83 + <strong>Composition shock</strong>: removing 70% of small-dwelling
84 + sales moves the raw median +12.3% but the index only −1.0%.
85 + </li>
86 + <li>
87 + <strong>Seasonality</strong>: no stable monthly seasonality — all
88 + series are NSA.
89 + </li>
90 + </ul>
91 +
92 + <h2>Reliability grades</h2>
93 + <table className="data">
94 + <thead>
95 + <tr><th>Grade</th><th>Meaning</th><th>Liquidity</th></tr>
96 + </thead>
97 + <tbody>
98 + <tr><td>A</td><td>Very strong; direct estimation, tight CI</td><td>≥ 150 tx/month</td></tr>
99 + <tr><td>B</td><td>Strong; direct estimation</td><td>75–149</td></tr>
100 + <tr><td>C</td><td>Moderate; direct estimation floor</td><td>40–74</td></tr>
101 + <tr><td>D</td><td>Thin; hierarchical shrinkage</td><td>15–39</td></tr>
102 + <tr><td>E</td><td>Model-implied only</td><td>&lt; 15</td></tr>
103 + </tbody>
104 + </table>
105 +
106 + <h2>Conventions</h2>
107 + <p>
108 + Base 2021 average = 100. Continuous monthly grid; the trailing month is
109 + flagged <code>is_partial_month</code> while registrations arrive. Every
110 + observation ships with a 95% interval, transaction count, effective
111 + sample size and shrinkage weight. First releases are preserved and
112 + revisions are queryable (<code>/v1/vintages</code>). Growth horizons:
113 + 1m, 3m, 6m, YoY.
114 + </p>
115 +
116 + <p className="note">
117 + Author: Simon-Pierre Boucher — contact@spboucher.ai · Key references:
118 + Hill, Scholz, Shimizu &amp; Steurer (2022) J. Official Statistics;
119 + Eurostat/IMF/OECD RPPI Handbook (2013); Bailey, Muth &amp; Nourse
120 + (1963); Case &amp; Shiller (1989).
121 + </p>
122 + </>
123 + );
124 +}
added web/app/opengraph-image.tsx +76 −0
@@ -0,0 +1,76 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/opengraph-image.tsx
7 + * Purpose : Generated Open Graph share image (1200×630, brand chart motif).
8 + * =============================================================================
9 + */
10 +import { ImageResponse } from "next/og";
11 +
12 +export const runtime = "edge";
13 +export const alt = "QHPI — Quebec Housing Price Index (monthly)";
14 +export const size = { width: 1200, height: 630 };
15 +export const contentType = "image/png";
16 +
17 +export default function OgImage() {
18 + return new ImageResponse(
19 + (
20 + <div
21 + style={{
22 + width: "100%", height: "100%", display: "flex",
23 + flexDirection: "column", justifyContent: "space-between",
24 + background: "linear-gradient(135deg, #fcfcfb 60%, #e8f0fb 100%)",
25 + padding: 64, fontFamily: "Georgia, serif",
26 + }}
27 + >
28 + <div style={{ display: "flex", alignItems: "center", gap: 20 }}>
29 + <div
30 + style={{
31 + width: 56, height: 56, borderRadius: 14, background: "#2a78d6",
32 + display: "flex", alignItems: "center", justifyContent: "center",
33 + }}
34 + >
35 + <svg width="34" height="34" viewBox="0 0 22 22">
36 + <path d="M5 14.5 L9 10.5 L12 12.5 L17 6.5" fill="none"
37 + stroke="#fff" strokeWidth="2.2" strokeLinecap="round"
38 + strokeLinejoin="round" />
39 + </svg>
40 + </div>
41 + <div style={{ fontSize: 44, fontWeight: 700, color: "#0b0b0b" }}>
42 + QHPI
43 + </div>
44 + <div style={{ fontSize: 24, color: "#716f66" }}>
45 + Quebec Housing Price Index · monthly
46 + </div>
47 + </div>
48 +
49 + <svg width="1072" height="270" viewBox="0 0 1072 270">
50 + <path
51 + d="M0 240 C 120 235, 180 200, 260 190 S 400 205, 470 185
52 + S 610 120, 700 110 S 850 70, 940 45 S 1030 25, 1072 15"
53 + fill="none" stroke="#2a78d6" strokeWidth="6" strokeLinecap="round"
54 + />
55 + <path
56 + d="M0 240 C 120 235, 180 200, 260 190 S 400 205, 470 185
57 + S 610 120, 700 110 S 850 70, 940 45 S 1030 25, 1072 15
58 + L 1072 270 L 0 270 Z"
59 + fill="rgba(42,120,214,0.12)"
60 + />
61 + </svg>
62 +
63 + <div style={{ display: "flex", justifyContent: "space-between",
64 + alignItems: "flex-end" }}>
65 + <div style={{ fontSize: 34, color: "#0b0b0b", maxWidth: 760 }}>
66 + Quebec housing prices, measured monthly, robustly.
67 + </div>
68 + <div style={{ fontSize: 20, color: "#716f66" }}>
69 + indexqc.house
70 + </div>
71 + </div>
72 + </div>
73 + ),
74 + size,
75 + );
76 +}
added web/app/page.tsx +238 −0
@@ -0,0 +1,238 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/app/page.tsx
7 + * Purpose : Overview — ticker tape, animated mega hero on a draw-in chart,
8 + * regional podium, metric tiles, pulse heatmap table.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { useEffect, useState } from "react";
14 +import { useRouter } from "next/navigation";
15 +import {
16 + fetchOverview,
17 + fetchSeries,
18 + fetchStats,
19 + marketReportUrl,
20 + type Observation,
21 + type OverviewRow,
22 + type SeriesStats,
23 +} from "../lib/api";
24 +import CountUp from "../components/CountUp";
25 +import HeroChart from "../components/HeroChart";
26 +import ReliabilityBadge from "../components/ReliabilityBadge";
27 +import { DashboardSkeleton } from "../components/Skeleton";
28 +import Sparkline from "../components/Sparkline";
29 +import Ticker from "../components/Ticker";
30 +
31 +function Delta({ v, suffix = "%" }: { v: number | null; suffix?: string }) {
32 + if (v == null) return <span className="delta">—</span>;
33 + const cls = v >= 0 ? "delta up" : "delta down";
34 + return (
35 + <span className={cls}>
36 + {v >= 0 ? "▲" : "▼"} {Math.abs(v).toFixed(2)}
37 + {suffix}
38 + </span>
39 + );
40 +}
41 +
42 +function heatStyle(v: number | null, lo: number, hi: number) {
43 + if (v == null) return {};
44 + if (v < 0) return { background: "rgba(227, 73, 72, 0.18)" };
45 + const ramp = ["#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf"];
46 + const t = hi === lo ? 0.5 : Math.max(0, Math.min(0.999, (v - lo) / (hi - lo)));
47 + const c = ramp[Math.floor(t * ramp.length)];
48 + return {
49 + background: c,
50 + color: c === "#256abf" || c === "#3987e5" ? "#fff" : "#0b0b0b",
51 + };
52 +}
53 +
54 +const TYPES = ["all", "unifamilial", "condo", "plex"];
55 +const MEDALS = ["🥇", "🥈", "🥉"];
56 +
57 +export default function Overview() {
58 + const router = useRouter();
59 + const [stats, setStats] = useState<SeriesStats | null>(null);
60 + const [rows, setRows] = useState<OverviewRow[]>([]);
61 + const [series, setSeries] = useState<Observation[]>([]);
62 + const [ptype, setPtype] = useState("all");
63 + const [error, setError] = useState<string | null>(null);
64 +
65 + useEffect(() => {
66 + Promise.all([
67 + fetchStats("quebec", ptype),
68 + fetchOverview("region", ptype),
69 + fetchSeries("quebec", ptype),
70 + ])
71 + .then(([s, o, se]) => {
72 + setStats(s);
73 + setRows(o.rows);
74 + setSeries(se.observations);
75 + setError(null);
76 + })
77 + .catch((e) => setError(String(e)));
78 + }, [ptype]);
79 +
80 + if (error)
81 + return (
82 + <div className="card" style={{ marginTop: 32 }}>
83 + <strong>API unreachable.</strong>
84 + <p className="note">Start it with <code>make api</code> — {error}</p>
85 + </div>
86 + );
87 + if (!stats) return <DashboardSkeleton />;
88 +
89 + const yoys = rows.map((r) => r.yoy_pct).filter((v): v is number => v != null);
90 + const lo = Math.min(...yoys, 0);
91 + const hi = Math.max(...yoys, 1);
92 + const podium = rows.slice(0, 3);
93 +
94 + return (
95 + <>
96 + <Ticker ptype={ptype} />
97 +
98 + <div className="hero">
99 + <div>
100 + <div className="hero-kicker">Month of {stats.period}</div>
101 + <h1>Quebec housing prices,<br />measured monthly, robustly.</h1>
102 + <p className="lede">
103 + Robust hedonic index (rolling-time-dummy) · base 2021 = 100 ·
104 + uncertainty always displayed
105 + </p>
106 + <div className="controls" style={{ margin: "16px 0 0" }}>
107 + <div className="seg" role="group" aria-label="Property type">
108 + {TYPES.map((t) => (
109 + <button key={t} aria-pressed={ptype === t}
110 + onClick={() => setPtype(t)}>
111 + {t}
112 + </button>
113 + ))}
114 + </div>
115 + <a className="primary" href={marketReportUrl(ptype)} target="_blank"
116 + rel="noreferrer">
117 + ⤓ Market report (PDF)
118 + </a>
119 + </div>
120 + </div>
121 + <div className="hero-mega">
122 + <span className="big">
123 + <CountUp value={stats.index} decimals={1} />
124 + </span>
125 + <span className="sub">
126 + QHPI-QC · 95% CI {stats.lower_95.toFixed(1)}–{stats.upper_95.toFixed(1)}
127 + {stats.at_record_high && (
128 + <span className="record-chip"> · ★ record high</span>
129 + )}
130 + </span>
131 + <span className="sub">
132 + <Delta v={stats.yoy_pct} /> YoY · <Delta v={stats.since_2021_pct} /> since 2021
133 + </span>
134 + </div>
135 + </div>
136 +
137 + <HeroChart observations={series} />
138 +
139 + <h2>Fastest-appreciating regions</h2>
140 + <div className="podium fade-up">
141 + {podium.map((r, i) => (
142 + <div key={r.geography_id}
143 + className={`card hoverable p${i + 1}`}
144 + style={{ cursor: "pointer" }}
145 + onClick={() =>
146 + router.push(`/explore?geography=${r.geography_id}&type=${ptype}`)}>
147 + <div style={{ display: "flex", justifyContent: "space-between",
148 + alignItems: "baseline" }}>
149 + <strong>{MEDALS[i]} {r.geography_name}</strong>
150 + <ReliabilityBadge grade={r.reliability_grade} />
151 + </div>
152 + <div style={{ display: "flex", justifyContent: "space-between",
153 + alignItems: "flex-end", marginTop: 8 }}>
154 + <div>
155 + <div className="stat-tile">
156 + <span className="value" style={{ fontSize: 26 }}>
157 + <Delta v={r.yoy_pct} />
158 + </span>
159 + <span className="note">
160 + index {r.index.toFixed(1)}
161 + {r.at_record_high ? " · at peak" : ""}
162 + </span>
163 + </div>
164 + </div>
165 + <Sparkline values={r.spark} width={110} height={34} />
166 + </div>
167 + </div>
168 + ))}
169 + </div>
170 +
171 + <div className="grid cols-8 fade-up d1" style={{ marginTop: 24 }}>
172 + {[
173 + ["Dollar value", stats.representative_value
174 + ? <CountUp key="v" value={stats.representative_value} decimals={0} prefix="$" />
175 + : <span key="v">—</span>],
176 + ["1 month", <Delta key="a" v={stats.monthly_pct} />],
177 + ["3 months", <Delta key="b" v={stats.three_month_pct} />],
178 + ["6 months", <Delta key="c" v={stats.six_month_pct} />],
179 + ["CAGR", <Delta key="d" v={stats.cagr_pct} />],
180 + ["Volatility 12m", <span key="f">{stats.volatility_12m_pct.toFixed(2)}%</span>],
181 + ["Sales 12m", <CountUp key="g" value={stats.volume_12m} decimals={0} />],
182 + ["$ volume 12m", <span key="h">${(stats.dollar_volume_12m / 1e9).toFixed(1)}B</span>],
183 + ].map(([label, val]) => (
184 + <div className="card hoverable stat-tile mini-tile" key={String(label)}>
185 + <span className="label">{label}</span>
186 + <span className="value">{val}</span>
187 + </div>
188 + ))}
189 + </div>
190 +
191 + <h2>Regional pulse — {ptype} · month of {stats.period}</h2>
192 + <div className="card fade-up d2" style={{ padding: "8px 12px", overflowX: "auto" }}>
193 + <table className="pulse">
194 + <thead>
195 + <tr>
196 + <th>Region</th><th>Trend</th><th>Index</th><th>1m</th><th>3m</th>
197 + <th>6m</th><th>YoY</th><th>Since 2021</th><th>Vs peak</th>
198 + <th>Sales 12m</th><th>$ value</th><th>Grade</th>
199 + </tr>
200 + </thead>
201 + <tbody>
202 + {rows.map((r) => (
203 + <tr key={r.geography_id}
204 + onClick={() =>
205 + router.push(`/explore?geography=${r.geography_id}&type=${ptype}`)}>
206 + <td>
207 + {r.geography_name}
208 + {r.at_record_high && <span className="record-chip"> ★</span>}
209 + </td>
210 + <td><Sparkline values={r.spark} width={84} height={22} /></td>
211 + <td>{r.index.toFixed(1)}</td>
212 + <td><Delta v={r.monthly_pct} /></td>
213 + <td><Delta v={r.three_month_pct} /></td>
214 + <td><Delta v={r.six_month_pct} /></td>
215 + <td className="heat" style={heatStyle(r.yoy_pct, lo, hi)}>
216 + {r.yoy_pct == null ? "—" : `${r.yoy_pct > 0 ? "+" : ""}${r.yoy_pct.toFixed(2)}%`}
217 + </td>
218 + <td><Delta v={r.since_2021_pct} /></td>
219 + <td>{r.drawdown_pct === 0 ? "peak" : `${r.drawdown_pct.toFixed(1)}%`}</td>
220 + <td>{r.volume_12m.toLocaleString()}</td>
221 + <td>{r.representative_value
222 + ? `$${Math.round(r.representative_value / 1000)}k` : "—"}</td>
223 + <td>{r.reliability_grade}</td>
224 + </tr>
225 + ))}
226 + </tbody>
227 + </table>
228 + </div>
229 + <p className="note" style={{ marginTop: 12 }}>
230 + ★ = record high · YoY column shaded by appreciation · click a row to
231 + explore the series · press <kbd style={{
232 + background: "var(--surface-2)", border: "1px solid var(--border)",
233 + borderRadius: 5, padding: "0 5px", fontSize: 11,
234 + }}>⌘K</kbd> to jump anywhere.
235 + </p>
236 + </>
237 + );
238 +}
added web/components/CommandPalette.tsx +146 −0
@@ -0,0 +1,146 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/CommandPalette.tsx
7 + * Purpose : ⌘K command palette — fuzzy jump to any series or page,
8 + * full keyboard navigation, zero dependencies.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14 +import { useRouter } from "next/navigation";
15 +import { fetchGeographies, type GeographyNode } from "../lib/api";
16 +
17 +interface Item {
18 + label: string;
19 + hint: string;
20 + href: string;
21 +}
22 +
23 +const PAGES: Item[] = [
24 + { label: "Overview", hint: "page", href: "/" },
25 + { label: "Explore", hint: "page", href: "/explore" },
26 + { label: "Compare", hint: "page", href: "/compare" },
27 + { label: "Map", hint: "page", href: "/map" },
28 + { label: "Methodology", hint: "page", href: "/methodology" },
29 + { label: "API docs", hint: "page", href: "/api-docs" },
30 +];
31 +const TYPES = ["all", "unifamilial", "condo", "plex"];
32 +
33 +function norm(s: string): string {
34 + return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
35 +}
36 +
37 +export default function CommandPalette() {
38 + const router = useRouter();
39 + const [open, setOpen] = useState(false);
40 + const [q, setQ] = useState("");
41 + const [sel, setSel] = useState(0);
42 + const [geos, setGeos] = useState<GeographyNode[]>([]);
43 + const inputRef = useRef<HTMLInputElement>(null);
44 + const listRef = useRef<HTMLUListElement>(null);
45 +
46 + useEffect(() => {
47 + const onKey = (e: KeyboardEvent) => {
48 + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
49 + e.preventDefault();
50 + setOpen((v) => !v);
51 + } else if (e.key === "Escape") {
52 + setOpen(false);
53 + }
54 + };
55 + window.addEventListener("keydown", onKey);
56 + return () => window.removeEventListener("keydown", onKey);
57 + }, []);
58 +
59 + useEffect(() => {
60 + if (open) {
61 + setQ("");
62 + setSel(0);
63 + setTimeout(() => inputRef.current?.focus(), 30);
64 + if (!geos.length)
65 + fetchGeographies().then((g) => setGeos(g.published_series)).catch(() => {});
66 + }
67 + }, [open, geos.length]);
68 +
69 + const items = useMemo<Item[]>(() => {
70 + const series: Item[] = geos.flatMap((g) =>
71 + g.property_types.filter((t) => TYPES.includes(t)).map((t) => ({
72 + label: `${g.geography_name} · ${t}`,
73 + hint: g.geography_level,
74 + href: `/explore?geography=${g.geography_id}&type=${t}`,
75 + })));
76 + const all = [...PAGES, ...series];
77 + if (!q) return all.slice(0, 12);
78 + const tokens = norm(q).split(/\s+/).filter(Boolean);
79 + return all
80 + .filter((i) => {
81 + const hay = norm(i.label + " " + i.hint);
82 + return tokens.every((t) => hay.includes(t));
83 + })
84 + .slice(0, 12);
85 + }, [geos, q]);
86 +
87 + const go = useCallback((item: Item) => {
88 + setOpen(false);
89 + router.push(item.href);
90 + }, [router]);
91 +
92 + const onInputKey = (e: React.KeyboardEvent) => {
93 + if (e.key === "ArrowDown") {
94 + e.preventDefault();
95 + setSel((s) => Math.min(s + 1, items.length - 1));
96 + } else if (e.key === "ArrowUp") {
97 + e.preventDefault();
98 + setSel((s) => Math.max(s - 1, 0));
99 + } else if (e.key === "Enter" && items[sel]) {
100 + go(items[sel]);
101 + }
102 + };
103 +
104 + useEffect(() => {
105 + listRef.current?.children[sel]?.scrollIntoView({ block: "nearest" });
106 + }, [sel]);
107 +
108 + if (!open) return null;
109 +
110 + return (
111 + <div className="palette-backdrop" onClick={() => setOpen(false)}
112 + role="dialog" aria-modal="true" aria-label="Command palette">
113 + <div className="palette" onClick={(e) => e.stopPropagation()}>
114 + <input
115 + ref={inputRef}
116 + value={q}
117 + onChange={(e) => { setQ(e.target.value); setSel(0); }}
118 + onKeyDown={onInputKey}
119 + placeholder="Jump to a series or page… (e.g. montreal condo)"
120 + aria-label="Search series and pages"
121 + className="palette-input"
122 + />
123 + <ul ref={listRef} className="palette-list" role="listbox">
124 + {items.map((it, i) => (
125 + <li key={it.href + it.label} role="option" aria-selected={i === sel}>
126 + <button
127 + className={"palette-item" + (i === sel ? " active" : "")}
128 + onMouseEnter={() => setSel(i)}
129 + onClick={() => go(it)}
130 + >
131 + <span>{it.label}</span>
132 + <span className="palette-hint">{it.hint}</span>
133 + </button>
134 + </li>
135 + ))}
136 + {!items.length && (
137 + <li className="palette-empty">No match — try a region or city name</li>
138 + )}
139 + </ul>
140 + <div className="palette-foot">
141 + <span>↑↓ navigate</span><span>↵ open</span><span>esc close</span>
142 + </div>
143 + </div>
144 + </div>
145 + );
146 +}
added web/components/CountUp.tsx +56 −0
@@ -0,0 +1,56 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/CountUp.tsx
7 + * Purpose : Animated number count-up (easeOut, reduced-motion aware).
8 + * =============================================================================
9 + */
10 +"use client";
11 +
12 +import { useEffect, useRef, useState } from "react";
13 +
14 +export default function CountUp({
15 + value,
16 + decimals = 1,
17 + duration = 1100,
18 + prefix = "",
19 + suffix = "",
20 +}: {
21 + value: number;
22 + decimals?: number;
23 + duration?: number;
24 + prefix?: string;
25 + suffix?: string;
26 +}) {
27 + const [display, setDisplay] = useState(0);
28 + const raf = useRef<number>(0);
29 +
30 + useEffect(() => {
31 + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
32 + setDisplay(value);
33 + return;
34 + }
35 + const t0 = performance.now();
36 + const tick = (t: number) => {
37 + const p = Math.min(1, (t - t0) / duration);
38 + const eased = 1 - Math.pow(1 - p, 3);
39 + setDisplay(value * eased);
40 + if (p < 1) raf.current = requestAnimationFrame(tick);
41 + };
42 + raf.current = requestAnimationFrame(tick);
43 + return () => cancelAnimationFrame(raf.current);
44 + }, [value, duration]);
45 +
46 + return (
47 + <span style={{ fontVariantNumeric: "tabular-nums" }}>
48 + {prefix}
49 + {display.toLocaleString("en-CA", {
50 + minimumFractionDigits: decimals,
51 + maximumFractionDigits: decimals,
52 + })}
53 + {suffix}
54 + </span>
55 + );
56 +}
added web/components/DataTable.tsx +119 −0
@@ -0,0 +1,119 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/DataTable.tsx
7 + * Purpose : Sortable, filterable observations table under each chart —
8 + * uncertainty (CI, grade) always shown.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { useMemo, useState } from "react";
14 +import type { Observation } from "../lib/api";
15 +
16 +type SortKey = "period" | "index_smoothed" | "monthly_pct" | "yoy_pct"
17 + | "transactions";
18 +
19 +const COLS: { key: SortKey | string; label: string; sortable: boolean }[] = [
20 + { key: "period", label: "Period", sortable: true },
21 + { key: "index_smoothed", label: "Index", sortable: true },
22 + { key: "ci", label: "95% CI", sortable: false },
23 + { key: "monthly_pct", label: "MoM", sortable: true },
24 + { key: "yoy_pct", label: "YoY", sortable: true },
25 + { key: "representative_value", label: "$ value", sortable: false },
26 + { key: "transactions", label: "Sales", sortable: true },
27 + { key: "reliability_grade", label: "Grade", sortable: false },
28 +];
29 +
30 +function pct(v: number | null): string {
31 + return v == null ? "—" : `${v > 0 ? "+" : ""}${v.toFixed(2)}%`;
32 +}
33 +
34 +export default function DataTable({ observations }: { observations: Observation[] }) {
35 + const [sortKey, setSortKey] = useState<SortKey>("period");
36 + const [desc, setDesc] = useState(true);
37 + const [filter, setFilter] = useState("");
38 +
39 + const rows = useMemo(() => {
40 + let r = observations;
41 + if (filter) r = r.filter((o) => o.period.includes(filter));
42 + return [...r].sort((a, b) => {
43 + const av = a[sortKey] ?? -Infinity;
44 + const bv = b[sortKey] ?? -Infinity;
45 + const cmp = typeof av === "string"
46 + ? String(av).localeCompare(String(bv))
47 + : Number(av) - Number(bv);
48 + return desc ? -cmp : cmp;
49 + });
50 + }, [observations, sortKey, desc, filter]);
51 +
52 + const onSort = (key: string, sortable: boolean) => {
53 + if (!sortable) return;
54 + if (sortKey === key) setDesc(!desc);
55 + else {
56 + setSortKey(key as SortKey);
57 + setDesc(true);
58 + }
59 + };
60 +
61 + return (
62 + <div>
63 + <div className="controls" style={{ margin: "10px 0" }}>
64 + <input
65 + type="text"
66 + placeholder="Filter period… (e.g. 2024)"
67 + value={filter}
68 + aria-label="Filter table by period"
69 + onChange={(e) => setFilter(e.target.value)}
70 + style={{
71 + background: "var(--surface-1)", color: "var(--text-primary)",
72 + border: "1px solid var(--border-strong)", borderRadius: 10,
73 + padding: "7px 12px", fontSize: 13.5, fontFamily: "inherit",
74 + }}
75 + />
76 + <span className="note">{rows.length} rows</span>
77 + </div>
78 + <div style={{ overflowX: "auto", maxHeight: 420, overflowY: "auto" }}>
79 + <table className="data" style={{ fontSize: 13 }}>
80 + <thead>
81 + <tr>
82 + {COLS.map((c) => (
83 + <th key={c.key}
84 + onClick={() => onSort(c.key, c.sortable)}
85 + style={{ cursor: c.sortable ? "pointer" : "default",
86 + userSelect: "none" }}
87 + aria-sort={sortKey === c.key
88 + ? (desc ? "descending" : "ascending") : "none"}>
89 + {c.label}
90 + {c.sortable && sortKey === c.key && (desc ? " ↓" : " ↑")}
91 + </th>
92 + ))}
93 + </tr>
94 + </thead>
95 + <tbody>
96 + {rows.map((o) => (
97 + <tr key={o.period}>
98 + <td>{o.period}{o.is_partial_month ? " *" : ""}</td>
99 + <td>{o.index_smoothed.toFixed(2)}</td>
100 + <td className="note">
101 + {o.lower_95.toFixed(1)}–{o.upper_95.toFixed(1)}
102 + </td>
103 + <td className={o.monthly_pct != null && o.monthly_pct < 0
104 + ? "delta down" : "delta up"}>{pct(o.monthly_pct)}</td>
105 + <td className={o.yoy_pct != null && o.yoy_pct < 0
106 + ? "delta down" : "delta up"}>{pct(o.yoy_pct)}</td>
107 + <td>{o.representative_value
108 + ? `$${Math.round(o.representative_value).toLocaleString("en-CA")}` : "—"}</td>
109 + <td>{o.transactions}</td>
110 + <td>{o.reliability_grade}</td>
111 + </tr>
112 + ))}
113 + </tbody>
114 + </table>
115 + </div>
116 + <p className="note" style={{ marginTop: 8 }}>* partial month (nowcast)</p>
117 + </div>
118 + );
119 +}
added web/components/HeroChart.tsx +112 −0
@@ -0,0 +1,112 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/HeroChart.tsx
7 + * Purpose : Full-width hero area chart with draw-in animation, CI band and
8 + * pulsing end point — geometry rebuilt to the measured width.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import { useEffect, useMemo, useRef, useState } from "react";
14 +import type { Observation } from "../lib/api";
15 +
16 +export default function HeroChart({ observations }: { observations: Observation[] }) {
17 + const wrapRef = useRef<HTMLDivElement>(null);
18 + const [width, setWidth] = useState(1120);
19 +
20 + useEffect(() => {
21 + const el = wrapRef.current;
22 + if (!el) return;
23 + const ro = new ResizeObserver((entries) => {
24 + const w = entries[0]?.contentRect.width;
25 + if (w) setWidth(Math.max(280, Math.round(w)));
26 + });
27 + ro.observe(el);
28 + return () => ro.disconnect();
29 + }, []);
30 +
31 + const small = width < 560;
32 + const W = width;
33 + const H = small ? 190 : 240;
34 + const PAD = { l: small ? 6 : 30, r: small ? 64 : 96, t: 24, b: 22 };
35 +
36 + const geom = useMemo(() => {
37 + const obs = observations.filter((o) => !o.is_partial_month);
38 + if (obs.length < 2) return null;
39 + const vals = obs.map((o) => o.index_smoothed);
40 + const los = obs.map((o) => o.lower_95);
41 + const his = obs.map((o) => o.upper_95);
42 + const min = Math.min(...los);
43 + const max = Math.max(...his);
44 + const x = (i: number) =>
45 + PAD.l + (i / (obs.length - 1)) * (W - PAD.l - PAD.r);
46 + const y = (v: number) =>
47 + H - PAD.b - ((v - min) / (max - min || 1)) * (H - PAD.t - PAD.b);
48 + const pts = vals.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`);
49 + const line = "M" + pts.join(" L");
50 + const area = line + ` L${x(obs.length - 1)},${H - PAD.b} L${x(0)},${H - PAD.b} Z`;
51 + const upper = his.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`);
52 + const lower = los
53 + .map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`)
54 + .reverse();
55 + const band = "M" + upper.join(" L") + " L" + lower.join(" L") + " Z";
56 + const lastObs = obs[obs.length - 1];
57 + const yearStep = small ? 2 : 1; // fewer year labels on narrow screens
58 + return {
59 + line, area, band,
60 + lastX: x(obs.length - 1), lastY: y(vals[vals.length - 1]),
61 + last: lastObs,
62 + years: obs
63 + .map((o, i) => ({ o, i }))
64 + .filter(({ o }) => o.period.endsWith("-01")
65 + && Number(o.period.slice(0, 4)) % yearStep === (small ? 0 : 0))
66 + .map(({ o, i }) => ({ label: o.period.slice(0, 4), x: x(i) })),
67 + };
68 + // eslint-disable-next-line react-hooks/exhaustive-deps
69 + }, [observations, W, H, PAD.l, PAD.r]);
70 +
71 + if (!geom) return <div ref={wrapRef} />;
72 +
73 + return (
74 + <div ref={wrapRef} className="hero-chart card"
75 + aria-label="Quebec index history chart" role="img">
76 + <svg viewBox={`0 0 ${W} ${H}`}
77 + style={{ width: "100%", height: "auto", display: "block" }}>
78 + <defs>
79 + <linearGradient id="heroFill" x1="0" y1="0" x2="0" y2="1">
80 + <stop offset="0%" stopColor="var(--series-1)" stopOpacity="0.28" />
81 + <stop offset="100%" stopColor="var(--series-1)" stopOpacity="0.02" />
82 + </linearGradient>
83 + </defs>
84 + <path d={geom.band} fill="var(--band)" className="hero-band" />
85 + <path d={geom.area} fill="url(#heroFill)" className="hero-area" />
86 + <path d={geom.line} fill="none" stroke="var(--series-1)"
87 + strokeWidth={small ? 2.2 : 2.6} strokeLinejoin="round"
88 + strokeLinecap="round" className="hero-line" pathLength={1} />
89 + {geom.years.map((yr) => (
90 + <g key={yr.label}>
91 + <line x1={yr.x} x2={yr.x} y1={H - PAD.b} y2={H - PAD.b + 4}
92 + stroke="var(--border-strong)" />
93 + <text x={yr.x} y={H - 6} fontSize={small ? 11 : 10.5}
94 + textAnchor="middle" fill="var(--text-muted)">{yr.label}</text>
95 + </g>
96 + ))}
97 + <circle cx={geom.lastX} cy={geom.lastY} r="4.5"
98 + fill="var(--series-1)" className="hero-dot" />
99 + <circle cx={geom.lastX} cy={geom.lastY} r="9"
100 + fill="var(--series-1)" opacity="0.25" className="hero-pulse" />
101 + <text x={geom.lastX + 12} y={geom.lastY - 5} fontSize={small ? 13 : 15}
102 + fontWeight="700" fill="var(--text-primary)" className="hero-dot">
103 + {geom.last.index_smoothed.toFixed(1)}
104 + </text>
105 + <text x={geom.lastX + 12} y={geom.lastY + 10} fontSize={small ? 9.5 : 10}
106 + fill="var(--text-muted)" className="hero-dot">
107 + {geom.last.period}
108 + </text>
109 + </svg>
110 + </div>
111 + );
112 +}
added web/components/IndexChart.tsx +274 −0
@@ -0,0 +1,274 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/IndexChart.tsx
7 + * Purpose : Weekly index chart — smoothed line, 95% CI band, optional raw
8 + * overlay, index-points or dollar-value unit, volume subchart.
9 + * =============================================================================
10 + */
11 +"use client";
12 +
13 +import {
14 + Area,
15 + Bar,
16 + BarChart,
17 + Brush,
18 + CartesianGrid,
19 + ComposedChart,
20 + Line,
21 + ReferenceLine,
22 + ResponsiveContainer,
23 + Tooltip,
24 + XAxis,
25 + YAxis,
26 +} from "recharts";
27 +import type { Observation } from "../lib/api";
28 +import type { MarketEvent } from "../lib/events";
29 +
30 +export type ChartUnit = "points" | "dollars";
31 +
32 +interface Props {
33 + observations: Observation[];
34 + showRaw?: boolean;
35 + showVolume?: boolean;
36 + unit?: ChartUnit;
37 + annotations?: MarketEvent[];
38 + showBrush?: boolean;
39 +}
40 +
41 +interface Point {
42 + period: string;
43 + smoothed: number;
44 + raw: number | null;
45 + band: [number, number];
46 + transactions: number;
47 + partial: boolean;
48 + mom: number | null;
49 +}
50 +
51 +function money(v: number): string {
52 + return "$" + Math.round(v).toLocaleString("en-CA");
53 +}
54 +
55 +function moneyCompact(v: number): string {
56 + return v >= 1_000_000
57 + ? "$" + (v / 1_000_000).toFixed(2) + "M"
58 + : "$" + Math.round(v / 1000) + "k";
59 +}
60 +
61 +function fmtVal(v: number | null | undefined, unit: ChartUnit): string {
62 + if (v == null || Number.isNaN(v)) return "—";
63 + return unit === "dollars" ? money(v) : v.toFixed(1);
64 +}
65 +
66 +function ChartTooltip({ active, payload, label, unit }: {
67 + active?: boolean;
68 + payload?: ReadonlyArray<{ payload?: unknown }>;
69 + label?: unknown;
70 + unit: ChartUnit;
71 +}) {
72 + const p = payload?.[0]?.payload as Point | undefined;
73 + if (!active || !p) return null;
74 + return (
75 + <div className="chart-tooltip">
76 + <div className="tt-label">
77 + Month of {String(label)}
78 + {p.partial ? " · partial" : ""}
79 + </div>
80 + <div className="tt-value">
81 + {fmtVal(p.smoothed, unit)}{" "}
82 + <span className="tt-ci">
83 + [{fmtVal(p.band[0], unit)} – {fmtVal(p.band[1], unit)}]
84 + </span>
85 + </div>
86 + {p.mom != null && (
87 + <div className={p.mom >= 0 ? "delta up" : "delta down"}
88 + style={{ fontSize: 12.5 }}>
89 + {p.mom >= 0 ? "▲" : "▼"} {Math.abs(p.mom).toFixed(2)}% vs previous month
90 + </div>
91 + )}
92 + {p.raw != null && (
93 + <div className="tt-raw">raw monthly: {fmtVal(p.raw, unit)}</div>
94 + )}
95 + <div className="tt-n">{p.transactions} transactions</div>
96 + </div>
97 + );
98 +}
99 +
100 +export default function IndexChart({
101 + observations,
102 + showRaw = false,
103 + showVolume = true,
104 + unit = "points",
105 + annotations = [],
106 + showBrush = false,
107 +}: Props) {
108 + // Fixed-basket conversion: representative_value = basket × index/100, so
109 + // one constant per cell converts every variant (raw, band) to dollars.
110 + const withRep = observations.find(
111 + (o) => o.representative_value != null && o.index_smoothed > 0,
112 + );
113 + const basket =
114 + unit === "dollars" && withRep
115 + ? (withRep.representative_value as number) / withRep.index_smoothed
116 + : 1 / 100;
117 + const k = unit === "dollars" ? basket : 1;
118 +
119 + const data: Point[] = observations.map((o, i) => ({
120 + period: o.period,
121 + smoothed: o.index_smoothed * (unit === "dollars" ? k : 1),
122 + raw: o.index == null ? null : o.index * (unit === "dollars" ? k : 1),
123 + band: [
124 + o.lower_95 * (unit === "dollars" ? k : 1),
125 + o.upper_95 * (unit === "dollars" ? k : 1),
126 + ],
127 + transactions: o.transactions,
128 + partial: o.is_partial_month,
129 + mom: i > 0
130 + ? (o.index_smoothed / observations[i - 1].index_smoothed - 1) * 100
131 + : null,
132 + }));
133 +
134 + const visibleEvents = annotations.filter((e) =>
135 + data.some((d) => d.period === e.period));
136 +
137 + // Scale to the index line, not the CI band: the first thin weeks carry
138 + // huge honest intervals that would otherwise crush the whole chart.
139 + const lineVals = data.flatMap((d) =>
140 + d.raw != null && showRaw ? [d.smoothed, d.raw] : [d.smoothed],
141 + );
142 + const lo = Math.min(...lineVals);
143 + const hi = Math.max(...lineVals);
144 + const pad = (hi - lo) * 0.12 || 5;
145 + const yDomain: [number, number] = [lo - pad, hi + pad];
146 +
147 + const tickFmt = (v: number) =>
148 + unit === "dollars" ? moneyCompact(v) : String(Math.round(v));
149 +
150 + return (
151 + <div>
152 + <div style={{ width: "100%", height: showBrush ? 396 : 340 }}
153 + role="img"
154 + aria-label={`Monthly index chart, ${data.length} observations with 95% confidence band`}>
155 + <ResponsiveContainer>
156 + <ComposedChart data={data} margin={{ top: 22, right: 12, left: 4, bottom: 0 }}>
157 + <CartesianGrid stroke="var(--border)" strokeDasharray="2 4" vertical={false} />
158 + <XAxis
159 + dataKey="period"
160 + tick={{ fill: "var(--text-muted)", fontSize: 12 }}
161 + tickLine={false}
162 + axisLine={{ stroke: "var(--border)" }}
163 + minTickGap={70}
164 + />
165 + <YAxis
166 + domain={yDomain}
167 + allowDataOverflow
168 + tickFormatter={tickFmt}
169 + tick={{ fill: "var(--text-muted)", fontSize: 12 }}
170 + tickLine={false}
171 + axisLine={false}
172 + width={unit === "dollars" ? 58 : 46}
173 + />
174 + <Tooltip
175 + content={(p) => <ChartTooltip {...p} unit={unit} />}
176 + cursor={{ stroke: "var(--text-muted)", strokeWidth: 1 }}
177 + />
178 + <Area
179 + dataKey="band"
180 + stroke="none"
181 + fill="var(--band)"
182 + isAnimationActive={false}
183 + name="95% CI"
184 + />
185 + {showRaw && (
186 + <Line
187 + dataKey="raw"
188 + stroke="var(--series-2)"
189 + strokeWidth={1}
190 + dot={false}
191 + connectNulls
192 + isAnimationActive={false}
193 + name="Raw monthly"
194 + />
195 + )}
196 + <Line
197 + dataKey="smoothed"
198 + stroke="var(--series-1)"
199 + strokeWidth={2}
200 + dot={false}
201 + isAnimationActive
202 + animationDuration={700}
203 + animationEasing="ease-out"
204 + name={unit === "dollars" ? "Representative value" : "Index (smoothed)"}
205 + />
206 + {visibleEvents.map((e) => (
207 + <ReferenceLine
208 + key={e.period}
209 + x={e.period}
210 + stroke="var(--text-muted)"
211 + strokeDasharray="4 4"
212 + strokeWidth={1}
213 + label={{
214 + value: e.short,
215 + position: "top",
216 + fill: "var(--text-muted)",
217 + fontSize: 9.5,
218 + }}
219 + />
220 + ))}
221 + {showBrush && (
222 + <Brush
223 + dataKey="period"
224 + height={26}
225 + travellerWidth={8}
226 + stroke="var(--border-strong)"
227 + fill="var(--surface-2)"
228 + />
229 + )}
230 + </ComposedChart>
231 + </ResponsiveContainer>
232 + </div>
233 + {showRaw && (
234 + <div className="note" style={{ display: "flex", gap: 16 }}>
235 + <span><span style={{ color: "var(--series-1)" }}>―</span>{" "}
236 + {unit === "dollars" ? "Representative value (smoothed)" : "Index (one-sided smoothed)"}</span>
237 + <span><span style={{ color: "var(--series-2)" }}>―</span> Raw monthly estimate</span>
238 + <span style={{ color: "var(--text-muted)" }}>▮ 95% CI</span>
239 + </div>
240 + )}
241 + {showVolume && (
242 + <div style={{ width: "100%", height: 90, marginTop: 8 }}>
243 + <ResponsiveContainer>
244 + <BarChart data={data} margin={{ top: 0, right: 12, left: 4, bottom: 0 }}>
245 + <XAxis dataKey="period" hide />
246 + <YAxis
247 + tick={{ fill: "var(--text-muted)", fontSize: 11 }}
248 + tickLine={false}
249 + axisLine={false}
250 + width={unit === "dollars" ? 58 : 46}
251 + />
252 + <Tooltip
253 + content={({ active, payload, label }) =>
254 + active && payload?.length ? (
255 + <div className="chart-tooltip">
256 + {label}: {(payload[0].payload as Point).transactions} transactions
257 + </div>
258 + ) : null
259 + }
260 + />
261 + <Bar
262 + dataKey="transactions"
263 + fill="var(--series-1)"
264 + opacity={0.55}
265 + isAnimationActive={false}
266 + name="Transactions"
267 + />
268 + </BarChart>
269 + </ResponsiveContainer>
270 + </div>
271 + )}
272 + </div>
273 + );
274 +}
added web/components/ReliabilityBadge.tsx +38 −0
@@ -0,0 +1,38 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/ReliabilityBadge.tsx
7 + * Purpose : Reliability grade badge (A–E) — icon + label, never color alone.
8 + * =============================================================================
9 + */
10 +
11 +const DESCRIPTIONS: Record<string, string> = {
12 + A: "Very strong — high liquidity, tight confidence band",
13 + B: "Strong — reliable weekly signal",
14 + C: "Moderate — partial shrinkage toward parent trend",
15 + D: "Thin — heavy shrinkage, interpret levels not weeks",
16 + E: "Model-implied — published for transparency only",
17 +};
18 +
19 +export default function ReliabilityBadge({ grade }: { grade: string }) {
20 + const good = grade === "A" || grade === "B";
21 + const weak = grade === "D" || grade === "E";
22 + return (
23 + <span className="badge" title={DESCRIPTIONS[grade] ?? ""}>
24 + <span
25 + className="dot"
26 + style={{
27 + background: good
28 + ? "var(--good)"
29 + : weak
30 + ? "var(--serious)"
31 + : "var(--series-4)",
32 + }}
33 + />
34 + Reliability {grade}
35 + {weak ? " ⚠" : ""}
36 + </span>
37 + );
38 +}
added web/components/SearchButton.tsx +24 −0
@@ -0,0 +1,24 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/SearchButton.tsx
7 + * Purpose : Header search button — dispatches the ⌘K palette shortcut.
8 + * =============================================================================
9 + */
10 +"use client";
11 +
12 +export default function SearchButton() {
13 + return (
14 + <button
15 + className="kbd-btn"
16 + aria-label="Open command palette"
17 + onClick={() =>
18 + window.dispatchEvent(
19 + new KeyboardEvent("keydown", { key: "k", metaKey: true }))}
20 + >
21 + Search <kbd>⌘K</kbd>
22 + </button>
23 + );
24 +}
added web/components/Skeleton.tsx +61 −0
@@ -0,0 +1,61 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/Skeleton.tsx
7 + * Purpose : Shimmer skeleton loaders — tiles, charts, tables (no "Loading…").
8 + * =============================================================================
9 + */
10 +
11 +export function SkeletonBlock({ h = 20, w = "100%", style = {} }: {
12 + h?: number; w?: number | string; style?: React.CSSProperties;
13 +}) {
14 + return (
15 + <div className="skeleton" aria-hidden
16 + style={{ height: h, width: w, ...style }} />
17 + );
18 +}
19 +
20 +export function TileSkeleton() {
21 + return (
22 + <div className="card stat-tile" aria-busy="true">
23 + <SkeletonBlock h={11} w="55%" />
24 + <SkeletonBlock h={32} w="70%" style={{ marginTop: 8 }} />
25 + <SkeletonBlock h={12} w="45%" style={{ marginTop: 6 }} />
26 + </div>
27 + );
28 +}
29 +
30 +export function ChartSkeleton({ height = 340 }: { height?: number }) {
31 + return (
32 + <div className="card" aria-busy="true" aria-label="Chart loading">
33 + <SkeletonBlock h={14} w={220} />
34 + <SkeletonBlock h={height - 60} style={{ marginTop: 14 }} />
35 + </div>
36 + );
37 +}
38 +
39 +export function TableSkeleton({ rows = 8 }: { rows?: number }) {
40 + return (
41 + <div className="card" aria-busy="true" style={{ padding: "10px 14px" }}>
42 + {Array.from({ length: rows }, (_, i) => (
43 + <SkeletonBlock key={i} h={16}
44 + style={{ marginTop: i ? 12 : 4, opacity: 1 - i * 0.09 }} />
45 + ))}
46 + </div>
47 + );
48 +}
49 +
50 +export function DashboardSkeleton() {
51 + return (
52 + <>
53 + <div className="grid cols-4" style={{ marginTop: 24 }}>
54 + {Array.from({ length: 4 }, (_, i) => <TileSkeleton key={i} />)}
55 + </div>
56 + <div style={{ marginTop: 16 }}>
57 + <TableSkeleton rows={10} />
58 + </div>
59 + </>
60 + );
61 +}
added web/components/Sparkline.tsx +43 −0
@@ -0,0 +1,43 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/Sparkline.tsx
7 + * Purpose : Inline SVG sparkline for stat tiles (no axes, one series).
8 + * =============================================================================
9 + */
10 +
11 +export default function Sparkline({
12 + values,
13 + width = 160,
14 + height = 40,
15 +}: {
16 + values: number[];
17 + width?: number;
18 + height?: number;
19 +}) {
20 + if (values.length < 2) return null;
21 + const min = Math.min(...values);
22 + const max = Math.max(...values);
23 + const span = max - min || 1;
24 + const pts = values
25 + .map((v, i) => {
26 + const x = (i / (values.length - 1)) * (width - 4) + 2;
27 + const y = height - 3 - ((v - min) / span) * (height - 6);
28 + return `${x.toFixed(1)},${y.toFixed(1)}`;
29 + })
30 + .join(" ");
31 + return (
32 + <svg width={width} height={height} role="img" aria-label="index sparkline">
33 + <polyline
34 + points={pts}
35 + fill="none"
36 + stroke="var(--series-1)"
37 + strokeWidth={2}
38 + strokeLinejoin="round"
39 + strokeLinecap="round"
40 + />
41 + </svg>
42 + );
43 +}
added web/components/ThemeToggle.tsx +42 −0
@@ -0,0 +1,42 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/ThemeToggle.tsx
7 + * Purpose : Light/dark theme toggle (data-theme attribute wins over OS).
8 + * =============================================================================
9 + */
10 +"use client";
11 +
12 +import { useEffect, useState } from "react";
13 +
14 +export default function ThemeToggle() {
15 + const [theme, setTheme] = useState<string | null>(null);
16 +
17 + useEffect(() => {
18 + const saved = localStorage.getItem("qwhpi-theme");
19 + if (saved) {
20 + setTheme(saved);
21 + document.documentElement.dataset.theme = saved;
22 + }
23 + }, []);
24 +
25 + const toggle = () => {
26 + const current =
27 + theme ??
28 + (window.matchMedia("(prefers-color-scheme: dark)").matches
29 + ? "dark"
30 + : "light");
31 + const next = current === "dark" ? "light" : "dark";
32 + setTheme(next);
33 + document.documentElement.dataset.theme = next;
34 + localStorage.setItem("qwhpi-theme", next);
35 + };
36 +
37 + return (
38 + <button className="ctrl" onClick={toggle} aria-label="Toggle color theme">
39 + {theme === "dark" ? "Light" : "Dark"} mode
40 + </button>
41 + );
42 +}
added web/components/Ticker.tsx +54 −0
@@ -0,0 +1,54 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/components/Ticker.tsx
7 + * Purpose : Bloomberg-style infinite ticker tape — regions & cities YoY.
8 + * =============================================================================
9 + */
10 +"use client";
11 +
12 +import { useEffect, useState } from "react";
13 +import Link from "next/link";
14 +import { fetchOverview, type OverviewRow } from "../lib/api";
15 +
16 +export default function Ticker({ ptype = "all" }: { ptype?: string }) {
17 + const [rows, setRows] = useState<OverviewRow[]>([]);
18 +
19 + useEffect(() => {
20 + Promise.all([
21 + fetchOverview("region", ptype),
22 + fetchOverview("municipality", ptype),
23 + ])
24 + .then(([r, m]) => setRows([...m.rows, ...r.rows]))
25 + .catch(() => setRows([]));
26 + }, [ptype]);
27 +
28 + if (!rows.length) return <div className="ticker-shell" aria-hidden />;
29 +
30 + const items = rows.map((r) => (
31 + <Link
32 + key={r.geography_id}
33 + className="ticker-item"
34 + href={`/explore?geography=${r.geography_id}&type=${ptype}`}
35 + >
36 + <span className="ticker-name">{r.geography_name}</span>
37 + <span className="ticker-val">{r.index.toFixed(1)}</span>
38 + <span className={r.yoy_pct != null && r.yoy_pct < 0 ? "delta down" : "delta up"}>
39 + {r.yoy_pct == null ? "—"
40 + : `${r.yoy_pct >= 0 ? "▲" : "▼"}${Math.abs(r.yoy_pct).toFixed(1)}%`}
41 + </span>
42 + </Link>
43 + ));
44 +
45 + return (
46 + <div className="ticker-shell" role="marquee"
47 + aria-label="Latest year-over-year appreciation by geography">
48 + <div className="ticker-track">
49 + <div className="ticker-group">{items}</div>
50 + <div className="ticker-group" aria-hidden>{items}</div>
51 + </div>
52 + </div>
53 + );
54 +}
added web/lib/api.ts +166 −0
@@ -0,0 +1,166 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/lib/api.ts
7 + * Purpose : Typed API client for the QHPI monthly FastAPI service.
8 + * =============================================================================
9 + */
10 +
11 +export const API_BASE =
12 + process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";
13 +
14 +export interface Observation {
15 + period: string;
16 + index: number | null;
17 + index_smoothed: number;
18 + representative_value: number | null;
19 + transactions: number;
20 + effective_sample_size: number | null;
21 + monthly_pct: number | null;
22 + three_month_pct: number | null;
23 + six_month_pct: number | null;
24 + yoy_pct: number | null;
25 + lower_95: number;
26 + upper_95: number;
27 + reliability_grade: string;
28 + shrinkage_weight: number | null;
29 + is_partial_month: boolean;
30 +}
31 +
32 +export interface SeriesResponse {
33 + geography: string;
34 + geography_name: string;
35 + geography_level: string;
36 + property_type: string;
37 + model_version: string;
38 + data_vintage: string;
39 + total_observations: number;
40 + observations: Observation[];
41 +}
42 +
43 +export interface LatestResponse {
44 + geography: string;
45 + geography_name: string;
46 + property_type: string;
47 + period: string;
48 + latest_index: number;
49 + representative_value: number | null;
50 + monthly_change: number | null;
51 + three_month_change: number | null;
52 + yoy_change: number | null;
53 + transactions: number;
54 + reliability: string;
55 + lower_95: number;
56 + upper_95: number;
57 + is_partial_month: boolean;
58 + data_vintage: string;
59 +}
60 +
61 +export interface GeographyNode {
62 + geography_id: string;
63 + geography_name: string;
64 + geography_level: string;
65 + property_types: string[];
66 +}
67 +
68 +export interface MapCell {
69 + geography_id: string;
70 + geography_name: string;
71 + value: number | null;
72 + reliability_grade: string;
73 + transactions: number;
74 +}
75 +
76 +async function get<T>(path: string): Promise<T> {
77 + const res = await fetch(`${API_BASE}${path}`, {
78 + headers: { Accept: "application/json" },
79 + });
80 + if (!res.ok) throw new Error(`${res.status} ${res.statusText} — ${path}`);
81 + return res.json() as Promise<T>;
82 +}
83 +
84 +export const fetchSeries = (g: string, t: string) =>
85 + get<SeriesResponse>(`/v1/index?geography=${g}&type=${t}`);
86 +
87 +export const fetchLatest = (g: string, t: string) =>
88 + get<LatestResponse>(`/v1/index/latest?geography=${g}&type=${t}`);
89 +
90 +export const fetchGeographies = () =>
91 + get<{ published_series: GeographyNode[]; data_vintage: string }>(
92 + "/v1/geographies",
93 + );
94 +
95 +export const fetchMap = (metric: string, type: string, period?: string) =>
96 + get<{ metric: string; period: string; cells: MapCell[] }>(
97 + `/v1/map?metric=${metric}&level=region&type=${type}` +
98 + (period ? `&period=${period}` : ""),
99 + );
100 +
101 +export const csvUrl = (g: string, t: string) =>
102 + `${API_BASE}/v1/index?geography=${g}&type=${t}&format=csv`;
103 +
104 +export interface SeriesStats {
105 + geography: string;
106 + property_type: string;
107 + period: string;
108 + index: number;
109 + representative_value: number | null;
110 + lower_95: number;
111 + upper_95: number;
112 + reliability_grade: string;
113 + monthly_pct: number | null;
114 + three_month_pct: number | null;
115 + six_month_pct: number | null;
116 + yoy_pct: number | null;
117 + since_2021_pct: number;
118 + cagr_pct: number;
119 + peak_index: number;
120 + peak_period: string;
121 + drawdown_pct: number;
122 + months_since_peak: number;
123 + at_record_high: boolean;
124 + volatility_12m_pct: number;
125 + momentum_3m_ann_pct: number;
126 + yoy_rank: number | null;
127 + yoy_rank_of: number;
128 + volume_12m: number;
129 + volume_yoy_pct: number | null;
130 + dollar_volume_12m: number;
131 + assessment_gap: number | null;
132 + effective_sample_size: number | null;
133 + spark: number[];
134 +}
135 +
136 +export interface OverviewRow {
137 + geography_id: string;
138 + geography_name: string;
139 + period: string;
140 + index: number;
141 + monthly_pct: number | null;
142 + three_month_pct: number | null;
143 + six_month_pct: number | null;
144 + yoy_pct: number | null;
145 + since_2021_pct: number;
146 + drawdown_pct: number;
147 + at_record_high: boolean;
148 + volume_12m: number;
149 + representative_value: number | null;
150 + reliability_grade: string;
151 + spark: number[];
152 +}
153 +
154 +export const fetchStats = (g: string, t: string) =>
155 + get<SeriesStats>(`/v1/stats?geography=${g}&type=${t}`);
156 +
157 +export const fetchOverview = (level: string, t: string) =>
158 + get<{ period: string; rows: OverviewRow[] }>(
159 + `/v1/stats/overview?level=${level}&type=${t}`,
160 + );
161 +
162 +export const reportUrl = (specs: string[]) =>
163 + `${API_BASE}/v1/report?series=${specs.join(",")}`;
164 +
165 +export const marketReportUrl = (t: string) =>
166 + `${API_BASE}/v1/report/market?type=${t}`;
added web/lib/events.ts +24 −0
@@ -0,0 +1,24 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/lib/events.ts
7 + * Purpose : Market event annotations (Bank of Canada policy milestones)
8 + * overlaid on index charts.
9 + * =============================================================================
10 + */
11 +
12 +export interface MarketEvent {
13 + period: string; // YYYY-MM
14 + label: string;
15 + short: string;
16 +}
17 +
18 +// Bank of Canada policy-rate milestones relevant to the 2021-2026 window.
19 +export const MARKET_EVENTS: MarketEvent[] = [
20 + { period: "2022-03", label: "BoC starts hiking (0.25% → 0.50%)", short: "First hike" },
21 + { period: "2022-07", label: "BoC +100 bp surprise hike", short: "+100 bp" },
22 + { period: "2023-07", label: "Policy rate peaks at 5.00%", short: "Peak 5%" },
23 + { period: "2024-06", label: "BoC first rate cut", short: "First cut" },
24 +];
added web/lib/exportPng.ts +77 −0
@@ -0,0 +1,77 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/lib/exportPng.ts
7 + * Purpose : Chart → PNG export (SVG serialization to canvas, no dependency).
8 + * =============================================================================
9 + */
10 +
11 +/** Resolve CSS custom properties so the serialized SVG is standalone. */
12 +function inlineColors(svg: SVGSVGElement): SVGSVGElement {
13 + const clone = svg.cloneNode(true) as SVGSVGElement;
14 + const styles = getComputedStyle(document.documentElement);
15 + const resolve = (v: string) =>
16 + v.replace(/var\((--[a-z0-9-]+)\)/gi, (_, name) =>
17 + styles.getPropertyValue(name).trim() || "#888");
18 + const walk = (el: Element) => {
19 + for (const attr of ["fill", "stroke", "color", "style"]) {
20 + const v = el.getAttribute(attr);
21 + if (v && v.includes("var(")) el.setAttribute(attr, resolve(v));
22 + }
23 + for (const child of Array.from(el.children)) walk(child);
24 + };
25 + walk(clone);
26 + return clone;
27 +}
28 +
29 +export async function exportChartPng(
30 + container: HTMLElement,
31 + filename: string,
32 + credit = "QHPI — indexqc.house · Simon-Pierre Boucher",
33 +): Promise<void> {
34 + const svg = container.querySelector<SVGSVGElement>("svg.recharts-surface, svg");
35 + if (!svg) return;
36 + const rect = svg.getBoundingClientRect();
37 + const scale = 2;
38 + const w = Math.round(rect.width);
39 + const h = Math.round(rect.height);
40 +
41 + const clone = inlineColors(svg);
42 + clone.setAttribute("width", String(w));
43 + clone.setAttribute("height", String(h));
44 + const xml = new XMLSerializer().serializeToString(clone);
45 + const url = URL.createObjectURL(
46 + new Blob([xml], { type: "image/svg+xml;charset=utf-8" }));
47 +
48 + const img = new Image();
49 + await new Promise((res, rej) => {
50 + img.onload = res;
51 + img.onerror = rej;
52 + img.src = url;
53 + });
54 +
55 + const surface = getComputedStyle(document.documentElement)
56 + .getPropertyValue("--surface-1").trim() || "#fff";
57 + const ink2 = getComputedStyle(document.documentElement)
58 + .getPropertyValue("--text-secondary").trim() || "#555";
59 +
60 + const canvas = document.createElement("canvas");
61 + canvas.width = w * scale;
62 + canvas.height = (h + 26) * scale;
63 + const ctx = canvas.getContext("2d")!;
64 + ctx.scale(scale, scale);
65 + ctx.fillStyle = surface;
66 + ctx.fillRect(0, 0, w, h + 26);
67 + ctx.drawImage(img, 0, 0, w, h);
68 + ctx.fillStyle = ink2;
69 + ctx.font = "10px system-ui, sans-serif";
70 + ctx.fillText(credit, 8, h + 16);
71 + URL.revokeObjectURL(url);
72 +
73 + const a = document.createElement("a");
74 + a.download = filename;
75 + a.href = canvas.toDataURL("image/png");
76 + a.click();
77 +}
added web/next.config.ts +27 −0
@@ -0,0 +1,27 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/next.config.ts
7 + * Purpose : Next.js configuration for the QWHPI dashboard.
8 + * =============================================================================
9 + */
10 +import type { NextConfig } from "next";
11 +
12 +const nextConfig: NextConfig = {
13 + reactStrictMode: true,
14 + // Single-domain deployments: the dashboard origin proxies API routes to the
15 + // local FastAPI process (set NEXT_PUBLIC_API_URL="" at build time).
16 + async rewrites() {
17 + const api = process.env.QWHPI_API_INTERNAL ?? "http://127.0.0.1:8080";
18 + return [
19 + { source: "/v1/:path*", destination: `${api}/v1/:path*` },
20 + { source: "/docs", destination: `${api}/docs` },
21 + { source: "/openapi.json", destination: `${api}/openapi.json` },
22 + { source: "/health", destination: `${api}/health` },
23 + ];
24 + },
25 +};
26 +
27 +export default nextConfig;
added web/package-lock.json +1413 −0
@@ -0,0 +1,1413 @@
1 +{
2 + "name": "qwhpi-web",
3 + "version": "0.1.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "qwhpi-web",
9 + "version": "0.1.0",
10 + "dependencies": {
11 + "next": "^15.3.0",
12 + "react": "^19.0.0",
13 + "react-dom": "^19.0.0",
14 + "recharts": "^2.15.0"
15 + },
16 + "devDependencies": {
17 + "@types/node": "^22",
18 + "@types/react": "^19",
19 + "@types/react-dom": "^19",
20 + "typescript": "^5.6"
21 + }
22 + },
23 + "node_modules/@babel/runtime": {
24 + "version": "7.29.7",
25 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
26 + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
27 + "license": "MIT",
28 + "engines": {
29 + "node": ">=6.9.0"
30 + }
31 + },
32 + "node_modules/@emnapi/runtime": {
33 + "version": "1.11.3",
34 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
35 + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
36 + "license": "MIT",
37 + "optional": true,
38 + "dependencies": {
39 + "tslib": "^2.4.0"
40 + }
41 + },
42 + "node_modules/@img/colour": {
43 + "version": "1.1.0",
44 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
45 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
46 + "license": "MIT",
47 + "optional": true,
48 + "engines": {
49 + "node": ">=18"
50 + }
51 + },
52 + "node_modules/@img/sharp-darwin-arm64": {
53 + "version": "0.34.5",
54 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
55 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
56 + "cpu": [
57 + "arm64"
58 + ],
59 + "license": "Apache-2.0",
60 + "optional": true,
61 + "os": [
62 + "darwin"
63 + ],
64 + "engines": {
65 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
66 + },
67 + "funding": {
68 + "url": "https://opencollective.com/libvips"
69 + },
70 + "optionalDependencies": {
71 + "@img/sharp-libvips-darwin-arm64": "1.2.4"
72 + }
73 + },
74 + "node_modules/@img/sharp-darwin-x64": {
75 + "version": "0.34.5",
76 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
77 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
78 + "cpu": [
79 + "x64"
80 + ],
81 + "license": "Apache-2.0",
82 + "optional": true,
83 + "os": [
84 + "darwin"
85 + ],
86 + "engines": {
87 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
88 + },
89 + "funding": {
90 + "url": "https://opencollective.com/libvips"
91 + },
92 + "optionalDependencies": {
93 + "@img/sharp-libvips-darwin-x64": "1.2.4"
94 + }
95 + },
96 + "node_modules/@img/sharp-libvips-darwin-arm64": {
97 + "version": "1.2.4",
98 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
99 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
100 + "cpu": [
101 + "arm64"
102 + ],
103 + "license": "LGPL-3.0-or-later",
104 + "optional": true,
105 + "os": [
106 + "darwin"
107 + ],
108 + "funding": {
109 + "url": "https://opencollective.com/libvips"
110 + }
111 + },
112 + "node_modules/@img/sharp-libvips-darwin-x64": {
113 + "version": "1.2.4",
114 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
115 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
116 + "cpu": [
117 + "x64"
118 + ],
119 + "license": "LGPL-3.0-or-later",
120 + "optional": true,
121 + "os": [
122 + "darwin"
123 + ],
124 + "funding": {
125 + "url": "https://opencollective.com/libvips"
126 + }
127 + },
128 + "node_modules/@img/sharp-libvips-linux-arm": {
129 + "version": "1.2.4",
130 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
131 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
132 + "cpu": [
133 + "arm"
134 + ],
135 + "libc": [
136 + "glibc"
137 + ],
138 + "license": "LGPL-3.0-or-later",
139 + "optional": true,
140 + "os": [
141 + "linux"
142 + ],
143 + "funding": {
144 + "url": "https://opencollective.com/libvips"
145 + }
146 + },
147 + "node_modules/@img/sharp-libvips-linux-arm64": {
148 + "version": "1.2.4",
149 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
150 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
151 + "cpu": [
152 + "arm64"
153 + ],
154 + "libc": [
155 + "glibc"
156 + ],
157 + "license": "LGPL-3.0-or-later",
158 + "optional": true,
159 + "os": [
160 + "linux"
161 + ],
162 + "funding": {
163 + "url": "https://opencollective.com/libvips"
164 + }
165 + },
166 + "node_modules/@img/sharp-libvips-linux-ppc64": {
167 + "version": "1.2.4",
168 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
169 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
170 + "cpu": [
171 + "ppc64"
172 + ],
173 + "libc": [
174 + "glibc"
175 + ],
176 + "license": "LGPL-3.0-or-later",
177 + "optional": true,
178 + "os": [
179 + "linux"
180 + ],
181 + "funding": {
182 + "url": "https://opencollective.com/libvips"
183 + }
184 + },
185 + "node_modules/@img/sharp-libvips-linux-riscv64": {
186 + "version": "1.2.4",
187 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
188 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
189 + "cpu": [
190 + "riscv64"
191 + ],
192 + "libc": [
193 + "glibc"
194 + ],
195 + "license": "LGPL-3.0-or-later",
196 + "optional": true,
197 + "os": [
198 + "linux"
199 + ],
200 + "funding": {
201 + "url": "https://opencollective.com/libvips"
202 + }
203 + },
204 + "node_modules/@img/sharp-libvips-linux-s390x": {
205 + "version": "1.2.4",
206 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
207 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
208 + "cpu": [
209 + "s390x"
210 + ],
211 + "libc": [
212 + "glibc"
213 + ],
214 + "license": "LGPL-3.0-or-later",
215 + "optional": true,
216 + "os": [
217 + "linux"
218 + ],
219 + "funding": {
220 + "url": "https://opencollective.com/libvips"
221 + }
222 + },
223 + "node_modules/@img/sharp-libvips-linux-x64": {
224 + "version": "1.2.4",
225 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
226 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
227 + "cpu": [
228 + "x64"
229 + ],
230 + "libc": [
231 + "glibc"
232 + ],
233 + "license": "LGPL-3.0-or-later",
234 + "optional": true,
235 + "os": [
236 + "linux"
237 + ],
238 + "funding": {
239 + "url": "https://opencollective.com/libvips"
240 + }
241 + },
242 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
243 + "version": "1.2.4",
244 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
245 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
246 + "cpu": [
247 + "arm64"
248 + ],
249 + "libc": [
250 + "musl"
251 + ],
252 + "license": "LGPL-3.0-or-later",
253 + "optional": true,
254 + "os": [
255 + "linux"
256 + ],
257 + "funding": {
258 + "url": "https://opencollective.com/libvips"
259 + }
260 + },
261 + "node_modules/@img/sharp-libvips-linuxmusl-x64": {
262 + "version": "1.2.4",
263 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
264 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
265 + "cpu": [
266 + "x64"
267 + ],
268 + "libc": [
269 + "musl"
270 + ],
271 + "license": "LGPL-3.0-or-later",
272 + "optional": true,
273 + "os": [
274 + "linux"
275 + ],
276 + "funding": {
277 + "url": "https://opencollective.com/libvips"
278 + }
279 + },
280 + "node_modules/@img/sharp-linux-arm": {
281 + "version": "0.34.5",
282 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
283 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
284 + "cpu": [
285 + "arm"
286 + ],
287 + "libc": [
288 + "glibc"
289 + ],
290 + "license": "Apache-2.0",
291 + "optional": true,
292 + "os": [
293 + "linux"
294 + ],
295 + "engines": {
296 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
297 + },
298 + "funding": {
299 + "url": "https://opencollective.com/libvips"
300 + },
301 + "optionalDependencies": {
302 + "@img/sharp-libvips-linux-arm": "1.2.4"
303 + }
304 + },
305 + "node_modules/@img/sharp-linux-arm64": {
306 + "version": "0.34.5",
307 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
308 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
309 + "cpu": [
310 + "arm64"
311 + ],
312 + "libc": [
313 + "glibc"
314 + ],
315 + "license": "Apache-2.0",
316 + "optional": true,
317 + "os": [
318 + "linux"
319 + ],
320 + "engines": {
321 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
322 + },
323 + "funding": {
324 + "url": "https://opencollective.com/libvips"
325 + },
326 + "optionalDependencies": {
327 + "@img/sharp-libvips-linux-arm64": "1.2.4"
328 + }
329 + },
330 + "node_modules/@img/sharp-linux-ppc64": {
331 + "version": "0.34.5",
332 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
333 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
334 + "cpu": [
335 + "ppc64"
336 + ],
337 + "libc": [
338 + "glibc"
339 + ],
340 + "license": "Apache-2.0",
341 + "optional": true,
342 + "os": [
343 + "linux"
344 + ],
345 + "engines": {
346 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
347 + },
348 + "funding": {
349 + "url": "https://opencollective.com/libvips"
350 + },
351 + "optionalDependencies": {
352 + "@img/sharp-libvips-linux-ppc64": "1.2.4"
353 + }
354 + },
355 + "node_modules/@img/sharp-linux-riscv64": {
356 + "version": "0.34.5",
357 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
358 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
359 + "cpu": [
360 + "riscv64"
361 + ],
362 + "libc": [
363 + "glibc"
364 + ],
365 + "license": "Apache-2.0",
366 + "optional": true,
367 + "os": [
368 + "linux"
369 + ],
370 + "engines": {
371 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
372 + },
373 + "funding": {
374 + "url": "https://opencollective.com/libvips"
375 + },
376 + "optionalDependencies": {
377 + "@img/sharp-libvips-linux-riscv64": "1.2.4"
378 + }
379 + },
380 + "node_modules/@img/sharp-linux-s390x": {
381 + "version": "0.34.5",
382 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
383 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
384 + "cpu": [
385 + "s390x"
386 + ],
387 + "libc": [
388 + "glibc"
389 + ],
390 + "license": "Apache-2.0",
391 + "optional": true,
392 + "os": [
393 + "linux"
394 + ],
395 + "engines": {
396 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
397 + },
398 + "funding": {
399 + "url": "https://opencollective.com/libvips"
400 + },
401 + "optionalDependencies": {
402 + "@img/sharp-libvips-linux-s390x": "1.2.4"
403 + }
404 + },
405 + "node_modules/@img/sharp-linux-x64": {
406 + "version": "0.34.5",
407 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
408 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
409 + "cpu": [
410 + "x64"
411 + ],
412 + "libc": [
413 + "glibc"
414 + ],
415 + "license": "Apache-2.0",
416 + "optional": true,
417 + "os": [
418 + "linux"
419 + ],
420 + "engines": {
421 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
422 + },
423 + "funding": {
424 + "url": "https://opencollective.com/libvips"
425 + },
426 + "optionalDependencies": {
427 + "@img/sharp-libvips-linux-x64": "1.2.4"
428 + }
429 + },
430 + "node_modules/@img/sharp-linuxmusl-arm64": {
431 + "version": "0.34.5",
432 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
433 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
434 + "cpu": [
435 + "arm64"
436 + ],
437 + "libc": [
438 + "musl"
439 + ],
440 + "license": "Apache-2.0",
441 + "optional": true,
442 + "os": [
443 + "linux"
444 + ],
445 + "engines": {
446 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
447 + },
448 + "funding": {
449 + "url": "https://opencollective.com/libvips"
450 + },
451 + "optionalDependencies": {
452 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
453 + }
454 + },
455 + "node_modules/@img/sharp-linuxmusl-x64": {
456 + "version": "0.34.5",
457 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
458 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
459 + "cpu": [
460 + "x64"
461 + ],
462 + "libc": [
463 + "musl"
464 + ],
465 + "license": "Apache-2.0",
466 + "optional": true,
467 + "os": [
468 + "linux"
469 + ],
470 + "engines": {
471 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
472 + },
473 + "funding": {
474 + "url": "https://opencollective.com/libvips"
475 + },
476 + "optionalDependencies": {
477 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
478 + }
479 + },
480 + "node_modules/@img/sharp-wasm32": {
481 + "version": "0.34.5",
482 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
483 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
484 + "cpu": [
485 + "wasm32"
486 + ],
487 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
488 + "optional": true,
489 + "dependencies": {
490 + "@emnapi/runtime": "^1.7.0"
491 + },
492 + "engines": {
493 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
494 + },
495 + "funding": {
496 + "url": "https://opencollective.com/libvips"
497 + }
498 + },
499 + "node_modules/@img/sharp-win32-arm64": {
500 + "version": "0.34.5",
501 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
502 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
503 + "cpu": [
504 + "arm64"
505 + ],
506 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
507 + "optional": true,
508 + "os": [
509 + "win32"
510 + ],
511 + "engines": {
512 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
513 + },
514 + "funding": {
515 + "url": "https://opencollective.com/libvips"
516 + }
517 + },
518 + "node_modules/@img/sharp-win32-ia32": {
519 + "version": "0.34.5",
520 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
521 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
522 + "cpu": [
523 + "ia32"
524 + ],
525 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
526 + "optional": true,
527 + "os": [
528 + "win32"
529 + ],
530 + "engines": {
531 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
532 + },
533 + "funding": {
534 + "url": "https://opencollective.com/libvips"
535 + }
536 + },
537 + "node_modules/@img/sharp-win32-x64": {
538 + "version": "0.34.5",
539 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
540 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
541 + "cpu": [
542 + "x64"
543 + ],
544 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
545 + "optional": true,
546 + "os": [
547 + "win32"
548 + ],
549 + "engines": {
550 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
551 + },
552 + "funding": {
553 + "url": "https://opencollective.com/libvips"
554 + }
555 + },
556 + "node_modules/@next/env": {
557 + "version": "15.5.23",
558 + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.23.tgz",
559 + "integrity": "sha512-Mv3Z9hVbFcPnoLevsZ6rnX1TBtyHb5E17yN7HTPDXSXxeNsGBjUFrdbjRXKKXIOhfth7/cg6Ay7PZ2UFawaWsQ==",
560 + "license": "MIT"
561 + },
562 + "node_modules/@next/swc-darwin-arm64": {
563 + "version": "15.5.23",
564 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.23.tgz",
565 + "integrity": "sha512-SrEwOROH/rhA03F59hHtdhgtfZMWGzr5duDBWgRQt2rS3mJhqMKOcnNx6txOd0/i3E3D3uFKYFvyHsEiwQxzag==",
566 + "cpu": [
567 + "arm64"
568 + ],
569 + "license": "MIT",
570 + "optional": true,
571 + "os": [
572 + "darwin"
573 + ],
574 + "engines": {
575 + "node": ">= 10"
576 + }
577 + },
578 + "node_modules/@next/swc-darwin-x64": {
579 + "version": "15.5.23",
580 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.23.tgz",
581 + "integrity": "sha512-f0FpFbG2EhDCuptBGcfrLcYMDuQAhe6m1QA4VVfXFrIBoFXvXt/olGbBkYkloKlXQtmhuzvtdYyuu/6zf07GIg==",
582 + "cpu": [
583 + "x64"
584 + ],
585 + "license": "MIT",
586 + "optional": true,
587 + "os": [
588 + "darwin"
589 + ],
590 + "engines": {
591 + "node": ">= 10"
592 + }
593 + },
594 + "node_modules/@next/swc-linux-arm64-gnu": {
595 + "version": "15.5.23",
596 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.23.tgz",
597 + "integrity": "sha512-WlNtfepUXKX2u2ZsJZ8c3c8+tJSRZqsYzoMwLOY72A8ucKCCgxgNhiePA3qzFYahVWrwcQd8jOeJmBinc+VFVQ==",
598 + "cpu": [
599 + "arm64"
600 + ],
601 + "libc": [
602 + "glibc"
603 + ],
604 + "license": "MIT",
605 + "optional": true,
606 + "os": [
607 + "linux"
608 + ],
609 + "engines": {
610 + "node": ">= 10"
611 + }
612 + },
613 + "node_modules/@next/swc-linux-arm64-musl": {
614 + "version": "15.5.23",
615 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.23.tgz",
616 + "integrity": "sha512-W/6qKk7UG93mg14PmQC+2urt69MIdwTBLNQ6MJyeC4wOCIHCjz+VfgssvS1pK7mgYBtLC1g6VKNoHD9xB0WWGg==",
617 + "cpu": [
618 + "arm64"
619 + ],
620 + "libc": [
621 + "musl"
622 + ],
623 + "license": "MIT",
624 + "optional": true,
625 + "os": [
626 + "linux"
627 + ],
628 + "engines": {
629 + "node": ">= 10"
630 + }
631 + },
632 + "node_modules/@next/swc-linux-x64-gnu": {
633 + "version": "15.5.23",
634 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.23.tgz",
635 + "integrity": "sha512-vzefI32mi6VMk96RaTAyxApgfGbiFzQBXVsekEjsDv1fr48mlABTWx0sUYhaYCBHWqCalxmz3DxbxFcbFvzNtw==",
636 + "cpu": [
637 + "x64"
638 + ],
639 + "libc": [
640 + "glibc"
641 + ],
642 + "license": "MIT",
643 + "optional": true,
644 + "os": [
645 + "linux"
646 + ],
647 + "engines": {
648 + "node": ">= 10"
649 + }
650 + },
651 + "node_modules/@next/swc-linux-x64-musl": {
652 + "version": "15.5.23",
653 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.23.tgz",
654 + "integrity": "sha512-qppK/3dTGOTI+aoWWBZc3DshFIhrzgL8guATlaN9V6M1QJxbkP/rhEZ22tdICsQ/2WWXopMZ2Jokzj2u3uKY3Q==",
655 + "cpu": [
656 + "x64"
657 + ],
658 + "libc": [
659 + "musl"
660 + ],
661 + "license": "MIT",
662 + "optional": true,
663 + "os": [
664 + "linux"
665 + ],
666 + "engines": {
667 + "node": ">= 10"
668 + }
669 + },
670 + "node_modules/@next/swc-win32-arm64-msvc": {
671 + "version": "15.5.23",
672 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.23.tgz",
673 + "integrity": "sha512-Wc29KFOdT7XBcII3Vtmw7aoU8Uk3Mes/FNJfhFeSHdYBFJWMcR/DsI8U9BCPUhq/uycsUVuqSKGthW15tLsigA==",
674 + "cpu": [
675 + "arm64"
676 + ],
677 + "license": "MIT",
678 + "optional": true,
679 + "os": [
680 + "win32"
681 + ],
682 + "engines": {
683 + "node": ">= 10"
684 + }
685 + },
686 + "node_modules/@next/swc-win32-x64-msvc": {
687 + "version": "15.5.23",
688 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.23.tgz",
689 + "integrity": "sha512-/C7wRW4fa9s/PKA18zGPPpVmx8ycgVpP8yOxro4gzGTzjPJdscbAP3ODeFvgiIovxD176Z2J/SXO9t8PJKHLeQ==",
690 + "cpu": [
691 + "x64"
692 + ],
693 + "license": "MIT",
694 + "optional": true,
695 + "os": [
696 + "win32"
697 + ],
698 + "engines": {
699 + "node": ">= 10"
700 + }
701 + },
702 + "node_modules/@swc/helpers": {
703 + "version": "0.5.15",
704 + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
705 + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
706 + "license": "Apache-2.0",
707 + "dependencies": {
708 + "tslib": "^2.8.0"
709 + }
710 + },
711 + "node_modules/@types/d3-array": {
712 + "version": "3.2.2",
713 + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
714 + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
715 + "license": "MIT"
716 + },
717 + "node_modules/@types/d3-color": {
718 + "version": "3.1.3",
719 + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
720 + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
721 + "license": "MIT"
722 + },
723 + "node_modules/@types/d3-ease": {
724 + "version": "3.0.2",
725 + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
726 + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
727 + "license": "MIT"
728 + },
729 + "node_modules/@types/d3-interpolate": {
730 + "version": "3.0.4",
731 + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
732 + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
733 + "license": "MIT",
734 + "dependencies": {
735 + "@types/d3-color": "*"
736 + }
737 + },
738 + "node_modules/@types/d3-path": {
739 + "version": "3.1.1",
740 + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
741 + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
742 + "license": "MIT"
743 + },
744 + "node_modules/@types/d3-scale": {
745 + "version": "4.0.9",
746 + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
747 + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
748 + "license": "MIT",
749 + "dependencies": {
750 + "@types/d3-time": "*"
751 + }
752 + },
753 + "node_modules/@types/d3-shape": {
754 + "version": "3.1.8",
755 + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
756 + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
757 + "license": "MIT",
758 + "dependencies": {
759 + "@types/d3-path": "*"
760 + }
761 + },
762 + "node_modules/@types/d3-time": {
763 + "version": "3.0.4",
764 + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
765 + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
766 + "license": "MIT"
767 + },
768 + "node_modules/@types/d3-timer": {
769 + "version": "3.0.2",
770 + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
771 + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
772 + "license": "MIT"
773 + },
774 + "node_modules/@types/node": {
775 + "version": "22.20.1",
776 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
777 + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
778 + "dev": true,
779 + "license": "MIT",
780 + "dependencies": {
781 + "undici-types": "~6.21.0"
782 + }
783 + },
784 + "node_modules/@types/react": {
785 + "version": "19.2.18",
786 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
787 + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
788 + "dev": true,
789 + "license": "MIT",
790 + "dependencies": {
791 + "csstype": "^3.2.2"
792 + }
793 + },
794 + "node_modules/@types/react-dom": {
795 + "version": "19.2.4",
796 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
797 + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
798 + "dev": true,
799 + "license": "MIT",
800 + "peerDependencies": {
801 + "@types/react": "^19.2.0"
802 + }
803 + },
804 + "node_modules/caniuse-lite": {
805 + "version": "1.0.30001807",
806 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz",
807 + "integrity": "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==",
808 + "funding": [
809 + {
810 + "type": "opencollective",
811 + "url": "https://opencollective.com/browserslist"
812 + },
813 + {
814 + "type": "tidelift",
815 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
816 + },
817 + {
818 + "type": "github",
819 + "url": "https://github.com/sponsors/ai"
820 + }
821 + ],
822 + "license": "CC-BY-4.0"
823 + },
824 + "node_modules/client-only": {
825 + "version": "0.0.1",
826 + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
827 + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
828 + "license": "MIT"
829 + },
830 + "node_modules/clsx": {
831 + "version": "2.1.1",
832 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
833 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
834 + "license": "MIT",
835 + "engines": {
836 + "node": ">=6"
837 + }
838 + },
839 + "node_modules/csstype": {
840 + "version": "3.2.3",
841 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
842 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
843 + "license": "MIT"
844 + },
845 + "node_modules/d3-array": {
846 + "version": "3.2.4",
847 + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
848 + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
849 + "license": "ISC",
850 + "dependencies": {
851 + "internmap": "1 - 2"
852 + },
853 + "engines": {
854 + "node": ">=12"
855 + }
856 + },
857 + "node_modules/d3-color": {
858 + "version": "3.1.0",
859 + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
860 + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
861 + "license": "ISC",
862 + "engines": {
863 + "node": ">=12"
864 + }
865 + },
866 + "node_modules/d3-ease": {
867 + "version": "3.0.1",
868 + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
869 + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
870 + "license": "BSD-3-Clause",
871 + "engines": {
872 + "node": ">=12"
873 + }
874 + },
875 + "node_modules/d3-format": {
876 + "version": "3.1.2",
877 + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
878 + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
879 + "license": "ISC",
880 + "engines": {
881 + "node": ">=12"
882 + }
883 + },
884 + "node_modules/d3-interpolate": {
885 + "version": "3.0.1",
886 + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
887 + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
888 + "license": "ISC",
889 + "dependencies": {
890 + "d3-color": "1 - 3"
891 + },
892 + "engines": {
893 + "node": ">=12"
894 + }
895 + },
896 + "node_modules/d3-path": {
897 + "version": "3.1.0",
898 + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
899 + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
900 + "license": "ISC",
901 + "engines": {
902 + "node": ">=12"
903 + }
904 + },
905 + "node_modules/d3-scale": {
906 + "version": "4.0.2",
907 + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
908 + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
909 + "license": "ISC",
910 + "dependencies": {
911 + "d3-array": "2.10.0 - 3",
912 + "d3-format": "1 - 3",
913 + "d3-interpolate": "1.2.0 - 3",
914 + "d3-time": "2.1.1 - 3",
915 + "d3-time-format": "2 - 4"
916 + },
917 + "engines": {
918 + "node": ">=12"
919 + }
920 + },
921 + "node_modules/d3-shape": {
922 + "version": "3.2.0",
923 + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
924 + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
925 + "license": "ISC",
926 + "dependencies": {
927 + "d3-path": "^3.1.0"
928 + },
929 + "engines": {
930 + "node": ">=12"
931 + }
932 + },
933 + "node_modules/d3-time": {
934 + "version": "3.1.0",
935 + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
936 + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
937 + "license": "ISC",
938 + "dependencies": {
939 + "d3-array": "2 - 3"
940 + },
941 + "engines": {
942 + "node": ">=12"
943 + }
944 + },
945 + "node_modules/d3-time-format": {
946 + "version": "4.1.0",
947 + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
948 + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
949 + "license": "ISC",
950 + "dependencies": {
951 + "d3-time": "1 - 3"
952 + },
953 + "engines": {
954 + "node": ">=12"
955 + }
956 + },
957 + "node_modules/d3-timer": {
958 + "version": "3.0.1",
959 + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
960 + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
961 + "license": "ISC",
962 + "engines": {
963 + "node": ">=12"
964 + }
965 + },
966 + "node_modules/decimal.js-light": {
967 + "version": "2.5.1",
968 + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
969 + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
970 + "license": "MIT"
971 + },
972 + "node_modules/detect-libc": {
973 + "version": "2.1.2",
974 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
975 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
976 + "license": "Apache-2.0",
977 + "optional": true,
978 + "engines": {
979 + "node": ">=8"
980 + }
981 + },
982 + "node_modules/dom-helpers": {
983 + "version": "5.2.1",
984 + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
985 + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
986 + "license": "MIT",
987 + "dependencies": {
988 + "@babel/runtime": "^7.8.7",
989 + "csstype": "^3.0.2"
990 + }
991 + },
992 + "node_modules/eventemitter3": {
993 + "version": "4.0.7",
994 + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
995 + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
996 + "license": "MIT"
997 + },
998 + "node_modules/fast-equals": {
999 + "version": "5.4.1",
1000 + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz",
1001 + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==",
1002 + "license": "MIT",
1003 + "engines": {
1004 + "node": ">=6.0.0"
1005 + }
1006 + },
1007 + "node_modules/internmap": {
1008 + "version": "2.0.3",
1009 + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
1010 + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
1011 + "license": "ISC",
1012 + "engines": {
1013 + "node": ">=12"
1014 + }
1015 + },
1016 + "node_modules/js-tokens": {
1017 + "version": "4.0.0",
1018 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1019 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1020 + "license": "MIT"
1021 + },
1022 + "node_modules/lodash": {
1023 + "version": "4.18.1",
1024 + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
1025 + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
1026 + "license": "MIT"
1027 + },
1028 + "node_modules/loose-envify": {
1029 + "version": "1.4.0",
1030 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1031 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1032 + "license": "MIT",
1033 + "dependencies": {
1034 + "js-tokens": "^3.0.0 || ^4.0.0"
1035 + },
1036 + "bin": {
1037 + "loose-envify": "cli.js"
1038 + }
1039 + },
1040 + "node_modules/nanoid": {
1041 + "version": "3.3.17",
1042 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
1043 + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
1044 + "funding": [
1045 + {
1046 + "type": "github",
1047 + "url": "https://github.com/sponsors/ai"
1048 + }
1049 + ],
1050 + "license": "MIT",
1051 + "bin": {
1052 + "nanoid": "bin/nanoid.cjs"
1053 + },
1054 + "engines": {
1055 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1056 + }
1057 + },
1058 + "node_modules/next": {
1059 + "version": "15.5.23",
1060 + "resolved": "https://registry.npmjs.org/next/-/next-15.5.23.tgz",
1061 + "integrity": "sha512-Gvd2WKgvxIXCGotxcI1im/Uf3rS3J3oZGw0g/uskg6AVBZhyE3aAbujkYWzS3xLmEPEtTLfkaVQUKK0KMTSIkA==",
1062 + "license": "MIT",
1063 + "dependencies": {
1064 + "@next/env": "15.5.23",
1065 + "@swc/helpers": "0.5.15",
1066 + "caniuse-lite": "^1.0.30001579",
1067 + "postcss": "8.4.31",
1068 + "styled-jsx": "5.1.6"
1069 + },
1070 + "bin": {
1071 + "next": "dist/bin/next"
1072 + },
1073 + "engines": {
1074 + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
1075 + },
1076 + "optionalDependencies": {
1077 + "@next/swc-darwin-arm64": "15.5.23",
1078 + "@next/swc-darwin-x64": "15.5.23",
1079 + "@next/swc-linux-arm64-gnu": "15.5.23",
1080 + "@next/swc-linux-arm64-musl": "15.5.23",
1081 + "@next/swc-linux-x64-gnu": "15.5.23",
1082 + "@next/swc-linux-x64-musl": "15.5.23",
1083 + "@next/swc-win32-arm64-msvc": "15.5.23",
1084 + "@next/swc-win32-x64-msvc": "15.5.23",
1085 + "sharp": "^0.34.3"
1086 + },
1087 + "peerDependencies": {
1088 + "@opentelemetry/api": "^1.1.0",
1089 + "@playwright/test": "^1.51.1",
1090 + "babel-plugin-react-compiler": "*",
1091 + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1092 + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
1093 + "sass": "^1.3.0"
1094 + },
1095 + "peerDependenciesMeta": {
1096 + "@opentelemetry/api": {
1097 + "optional": true
1098 + },
1099 + "@playwright/test": {
1100 + "optional": true
1101 + },
1102 + "babel-plugin-react-compiler": {
1103 + "optional": true
1104 + },
1105 + "sass": {
1106 + "optional": true
1107 + }
1108 + }
1109 + },
1110 + "node_modules/object-assign": {
1111 + "version": "4.1.1",
1112 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1113 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1114 + "license": "MIT",
1115 + "engines": {
1116 + "node": ">=0.10.0"
1117 + }
1118 + },
1119 + "node_modules/picocolors": {
1120 + "version": "1.1.1",
1121 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1122 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1123 + "license": "ISC"
1124 + },
1125 + "node_modules/postcss": {
1126 + "version": "8.4.31",
1127 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
1128 + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
1129 + "funding": [
1130 + {
1131 + "type": "opencollective",
1132 + "url": "https://opencollective.com/postcss/"
1133 + },
1134 + {
1135 + "type": "tidelift",
1136 + "url": "https://tidelift.com/funding/github/npm/postcss"
1137 + },
1138 + {
1139 + "type": "github",
1140 + "url": "https://github.com/sponsors/ai"
1141 + }
1142 + ],
1143 + "license": "MIT",
1144 + "dependencies": {
1145 + "nanoid": "^3.3.6",
1146 + "picocolors": "^1.0.0",
1147 + "source-map-js": "^1.0.2"
1148 + },
1149 + "engines": {
1150 + "node": "^10 || ^12 || >=14"
1151 + }
1152 + },
1153 + "node_modules/prop-types": {
1154 + "version": "15.8.1",
1155 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
1156 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
1157 + "license": "MIT",
1158 + "dependencies": {
1159 + "loose-envify": "^1.4.0",
1160 + "object-assign": "^4.1.1",
1161 + "react-is": "^16.13.1"
1162 + }
1163 + },
1164 + "node_modules/prop-types/node_modules/react-is": {
1165 + "version": "16.13.1",
1166 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
1167 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
1168 + "license": "MIT"
1169 + },
1170 + "node_modules/react": {
1171 + "version": "19.2.8",
1172 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
1173 + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
1174 + "license": "MIT",
1175 + "engines": {
1176 + "node": ">=0.10.0"
1177 + }
1178 + },
1179 + "node_modules/react-dom": {
1180 + "version": "19.2.8",
1181 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
1182 + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
1183 + "license": "MIT",
1184 + "dependencies": {
1185 + "scheduler": "^0.27.0"
1186 + },
1187 + "peerDependencies": {
1188 + "react": "^19.2.8"
1189 + }
1190 + },
1191 + "node_modules/react-is": {
1192 + "version": "18.3.1",
1193 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
1194 + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
1195 + "license": "MIT"
1196 + },
1197 + "node_modules/react-smooth": {
1198 + "version": "4.0.4",
1199 + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
1200 + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
1201 + "license": "MIT",
1202 + "dependencies": {
1203 + "fast-equals": "^5.0.1",
1204 + "prop-types": "^15.8.1",
1205 + "react-transition-group": "^4.4.5"
1206 + },
1207 + "peerDependencies": {
1208 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1209 + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1210 + }
1211 + },
1212 + "node_modules/react-transition-group": {
1213 + "version": "4.4.5",
1214 + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
1215 + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
1216 + "license": "BSD-3-Clause",
1217 + "dependencies": {
1218 + "@babel/runtime": "^7.5.5",
1219 + "dom-helpers": "^5.0.1",
1220 + "loose-envify": "^1.4.0",
1221 + "prop-types": "^15.6.2"
1222 + },
1223 + "peerDependencies": {
1224 + "react": ">=16.6.0",
1225 + "react-dom": ">=16.6.0"
1226 + }
1227 + },
1228 + "node_modules/recharts": {
1229 + "version": "2.15.4",
1230 + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
1231 + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
1232 + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide",
1233 + "license": "MIT",
1234 + "dependencies": {
1235 + "clsx": "^2.0.0",
1236 + "eventemitter3": "^4.0.1",
1237 + "lodash": "^4.17.21",
1238 + "react-is": "^18.3.1",
1239 + "react-smooth": "^4.0.4",
1240 + "recharts-scale": "^0.4.4",
1241 + "tiny-invariant": "^1.3.1",
1242 + "victory-vendor": "^36.6.8"
1243 + },
1244 + "engines": {
1245 + "node": ">=14"
1246 + },
1247 + "peerDependencies": {
1248 + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1249 + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1250 + }
1251 + },
1252 + "node_modules/recharts-scale": {
1253 + "version": "0.4.5",
1254 + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
1255 + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
1256 + "license": "MIT",
1257 + "dependencies": {
1258 + "decimal.js-light": "^2.4.1"
1259 + }
1260 + },
1261 + "node_modules/scheduler": {
1262 + "version": "0.27.0",
1263 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1264 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1265 + "license": "MIT"
1266 + },
1267 + "node_modules/semver": {
1268 + "version": "7.8.5",
1269 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
1270 + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
1271 + "license": "ISC",
1272 + "optional": true,
1273 + "bin": {
1274 + "semver": "bin/semver.js"
1275 + },
1276 + "engines": {
1277 + "node": ">=10"
1278 + }
1279 + },
1280 + "node_modules/sharp": {
1281 + "version": "0.34.5",
1282 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1283 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1284 + "hasInstallScript": true,
1285 + "license": "Apache-2.0",
1286 + "optional": true,
1287 + "dependencies": {
1288 + "@img/colour": "^1.0.0",
1289 + "detect-libc": "^2.1.2",
1290 + "semver": "^7.7.3"
1291 + },
1292 + "engines": {
1293 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1294 + },
1295 + "funding": {
1296 + "url": "https://opencollective.com/libvips"
1297 + },
1298 + "optionalDependencies": {
1299 + "@img/sharp-darwin-arm64": "0.34.5",
1300 + "@img/sharp-darwin-x64": "0.34.5",
1301 + "@img/sharp-libvips-darwin-arm64": "1.2.4",
1302 + "@img/sharp-libvips-darwin-x64": "1.2.4",
1303 + "@img/sharp-libvips-linux-arm": "1.2.4",
1304 + "@img/sharp-libvips-linux-arm64": "1.2.4",
1305 + "@img/sharp-libvips-linux-ppc64": "1.2.4",
1306 + "@img/sharp-libvips-linux-riscv64": "1.2.4",
1307 + "@img/sharp-libvips-linux-s390x": "1.2.4",
1308 + "@img/sharp-libvips-linux-x64": "1.2.4",
1309 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1310 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1311 + "@img/sharp-linux-arm": "0.34.5",
1312 + "@img/sharp-linux-arm64": "0.34.5",
1313 + "@img/sharp-linux-ppc64": "0.34.5",
1314 + "@img/sharp-linux-riscv64": "0.34.5",
1315 + "@img/sharp-linux-s390x": "0.34.5",
1316 + "@img/sharp-linux-x64": "0.34.5",
1317 + "@img/sharp-linuxmusl-arm64": "0.34.5",
1318 + "@img/sharp-linuxmusl-x64": "0.34.5",
1319 + "@img/sharp-wasm32": "0.34.5",
1320 + "@img/sharp-win32-arm64": "0.34.5",
1321 + "@img/sharp-win32-ia32": "0.34.5",
1322 + "@img/sharp-win32-x64": "0.34.5"
1323 + }
1324 + },
1325 + "node_modules/source-map-js": {
1326 + "version": "1.2.1",
1327 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1328 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1329 + "license": "BSD-3-Clause",
1330 + "engines": {
1331 + "node": ">=0.10.0"
1332 + }
1333 + },
1334 + "node_modules/styled-jsx": {
1335 + "version": "5.1.6",
1336 + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
1337 + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
1338 + "license": "MIT",
1339 + "dependencies": {
1340 + "client-only": "0.0.1"
1341 + },
1342 + "engines": {
1343 + "node": ">= 12.0.0"
1344 + },
1345 + "peerDependencies": {
1346 + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
1347 + },
1348 + "peerDependenciesMeta": {
1349 + "@babel/core": {
1350 + "optional": true
1351 + },
1352 + "babel-plugin-macros": {
1353 + "optional": true
1354 + }
1355 + }
1356 + },
1357 + "node_modules/tiny-invariant": {
1358 + "version": "1.3.3",
1359 + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
1360 + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
1361 + "license": "MIT"
1362 + },
1363 + "node_modules/tslib": {
1364 + "version": "2.8.1",
1365 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1366 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1367 + "license": "0BSD"
1368 + },
1369 + "node_modules/typescript": {
1370 + "version": "5.9.3",
1371 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1372 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1373 + "dev": true,
1374 + "license": "Apache-2.0",
1375 + "bin": {
1376 + "tsc": "bin/tsc",
1377 + "tsserver": "bin/tsserver"
1378 + },
1379 + "engines": {
1380 + "node": ">=14.17"
1381 + }
1382 + },
1383 + "node_modules/undici-types": {
1384 + "version": "6.21.0",
1385 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1386 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1387 + "dev": true,
1388 + "license": "MIT"
1389 + },
1390 + "node_modules/victory-vendor": {
1391 + "version": "36.9.2",
1392 + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
1393 + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
1394 + "license": "MIT AND ISC",
1395 + "dependencies": {
1396 + "@types/d3-array": "^3.0.3",
1397 + "@types/d3-ease": "^3.0.0",
1398 + "@types/d3-interpolate": "^3.0.1",
1399 + "@types/d3-scale": "^4.0.2",
1400 + "@types/d3-shape": "^3.1.0",
1401 + "@types/d3-time": "^3.0.0",
1402 + "@types/d3-timer": "^3.0.0",
1403 + "d3-array": "^3.1.6",
1404 + "d3-ease": "^3.0.1",
1405 + "d3-interpolate": "^3.0.1",
1406 + "d3-scale": "^4.0.2",
1407 + "d3-shape": "^3.1.0",
1408 + "d3-time": "^3.0.0",
1409 + "d3-timer": "^3.0.1"
1410 + }
1411 + }
1412 + }
1413 +}
added web/package.json +25 −0
@@ -0,0 +1,25 @@
1 +{
2 + "name": "qwhpi-web",
3 + "version": "0.1.0",
4 + "private": true,
5 + "author": "Simon-Pierre Boucher <contact@spboucher.ai>",
6 + "description": "QWHPI — Quebec Weekly Housing Price Index dashboard",
7 + "scripts": {
8 + "dev": "next dev -p 3000",
9 + "build": "next build",
10 + "start": "next start -p 3000",
11 + "typecheck": "tsc --noEmit"
12 + },
13 + "dependencies": {
14 + "next": "^15.3.0",
15 + "react": "^19.0.0",
16 + "react-dom": "^19.0.0",
17 + "recharts": "^2.15.0"
18 + },
19 + "devDependencies": {
20 + "@types/node": "^22",
21 + "@types/react": "^19",
22 + "@types/react-dom": "^19",
23 + "typescript": "^5.6"
24 + }
25 +}
added web/public/regions.geojson +24 −0
@@ -0,0 +1,24 @@
1 +{
2 +"type": "FeatureCollection",
3 +"name": "regions",
4 +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
5 +"features": [
6 +{ "type": "Feature", "properties": { "code": "01", "name": "Bas-Saint-Laurent", "geography_id": "region-01" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -66.832032993541887, 48.985077062530785 ], [ -67.153339142634252, 49.18906403276106 ], [ -67.653527346887046, 49.059287454794955 ], [ -68.467535729666494, 48.75809417676782 ], [ -68.999999195820209, 48.451724286567014 ], [ -69.693620092208832, 47.968393032985418 ], [ -69.910550122046899, 47.679365339581082 ], [ -70.065892232692377, 47.568336902140452 ], [ -70.197392286185178, 47.4119438131845 ], [ -69.625290475616112, 47.06672887090172 ], [ -69.224461123831304, 47.459833326149941 ], [ -69.043216763815181, 47.426783496102914 ], [ -69.051000494931756, 47.30017487489242 ], [ -68.569350023804873, 47.42700755922386 ], [ -68.382971782473021, 47.553346848707577 ], [ -68.383096827856065, 47.916549275197688 ], [ -68.122891146874991, 47.916472575997602 ], [ -68.122878233791909, 47.999961596642954 ], [ -67.597865906355949, 48.000126746595569 ], [ -67.58697761495371, 48.049075921609386 ], [ -67.666736277613239, 48.118481110138987 ], [ -67.374603194612703, 48.194892593960361 ], [ -67.277547792467402, 48.031538031877908 ], [ -66.879636407319737, 48.136954025567931 ], [ -66.934625790649989, 48.181190786992964 ], [ -66.900450576427687, 48.200447649982998 ], [ -66.875745542117869, 48.183041803330852 ], [ -66.839555950616386, 48.14709693205026 ], [ -66.836925357775854, 48.147741707786416 ], [ -66.97415999578601, 48.30315642898266 ], [ -65.948498198459305, 48.567089144875254 ], [ -66.537315142547982, 48.957826533637991 ], [ -66.70441521193213, 48.902873345905128 ], [ -66.699318005571314, 48.988611913225242 ], [ -66.732989366008425, 49.017530356093509 ], [ -66.832032993541887, 48.985077062530785 ] ] ] } },
7 +{ "type": "Feature", "properties": { "code": "02", "name": "Saguenay–Lac-Saint-Jean", "geography_id": "region-02" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -70.029889608212372, 52.83208781270055 ], [ -70.127352769308857, 52.810498021600203 ], [ -70.158019118544559, 52.75487579108664 ], [ -70.123012782802505, 52.751791827643387 ], [ -70.220665563779335, 52.710960459256881 ], [ -70.180804913611325, 52.662876248978954 ], [ -70.229060412544442, 52.635864638324705 ], [ -70.169669053109814, 52.603941585881003 ], [ -70.242708985221583, 52.559855241160008 ], [ -70.172981365334763, 52.542780077009425 ], [ -70.289708183364212, 52.418971922267225 ], [ -70.231401904774387, 52.417487173316175 ], [ -70.278686694660578, 52.353070389692697 ], [ -70.482013678038712, 52.303945326491949 ], [ -70.559851046036215, 52.317121283427525 ], [ -70.792275564174631, 52.233678726260095 ], [ -70.825558391191791, 52.247701291228815 ], [ -70.806851262208056, 52.2735753367147 ], [ -70.881018687776219, 52.278978001270332 ], [ -70.929532312266929, 52.226715597023762 ], [ -71.07753627155752, 52.211205816461593 ], [ -71.250263046222429, 52.066007520793178 ], [ -71.265350359872059, 52.037768716693598 ], [ -71.206599369705856, 52.039486030044223 ], [ -71.306620968385985, 51.971610647225681 ], [ -71.3852521847368, 51.842201289055396 ], [ -71.574183594959507, 51.814196502991223 ], [ -71.55163721173831, 51.789169887684984 ], [ -71.610667446443827, 51.692744197442295 ], [ -71.580147511277758, 51.509344178706982 ], [ -71.706026800931696, 51.439817932755702 ], [ -71.687931897139904, 51.401459025141072 ], [ -71.756300369151049, 51.339186720477755 ], [ -71.821280149824545, 51.373384685080055 ], [ -71.868619034664235, 51.343983305762556 ], [ -71.870120311300226, 51.277878975881833 ], [ -71.931866547968809, 51.275410434941186 ], [ -71.991243461538986, 51.230642722087758 ], [ -71.977866179604916, 51.211628790892647 ], [ -72.012993551621364, 51.220198781014162 ], [ -72.018682532877833, 51.138722835656104 ], [ -72.074140566569952, 51.150687671662489 ], [ -72.144714027195946, 51.069418869200177 ], [ -72.156027500507335, 50.978592714760488 ], [ -72.197975992391449, 50.99041784347564 ], [ -72.191667192447809, 51.029393868435434 ], [ -72.261556930901662, 51.00359071900008 ], [ -72.269595334337367, 50.951076254882999 ], [ -72.339739183559004, 50.885669226339608 ], [ -72.378226629827353, 50.888110075453852 ], [ -72.397509811883793, 50.813325279625538 ], [ -72.470280017219338, 50.784273812598528 ], [ -72.480711567244668, 50.750879134025659 ], [ -72.573375832499508, 50.750117395743501 ], [ -72.567190839113778, 50.885429230729166 ], [ -72.648286737839953, 50.747648244154433 ], [ -72.703726555891663, 50.781506467216389 ], [ -72.731350348729706, 50.761784250765459 ], [ -72.722162721569404, 50.798718593103452 ], [ -72.752005133560417, 50.820910537914862 ], [ -72.857900681641425, 50.812444509634709 ], [ -72.934312324222645, 50.762943434103612 ], [ -72.94096405364148, 50.713409452546145 ], [ -73.044625933807069, 50.654459722823731 ], [ -73.023141474548538, 50.697911788954464 ], [ -73.14707242557698, 50.750542239321447 ], [ -73.183699247213667, 50.701483754462252 ], [ -73.250414842093278, 50.692424398094076 ], [ -73.459726961931906, 50.368210812327739 ], [ -73.486034764580779, 50.400580425687487 ], [ -73.57361514593191, 50.359636563220278 ], [ -73.593429194707852, 50.403465048617775 ], [ -73.594997076396297, 50.349585265215225 ], [ -73.684497464680973, 50.21168373727172 ], [ -73.770337074998793, 50.2153264599404 ], [ -73.787170982234855, 50.056495629589449 ], [ -74.016589415838837, 49.801872518293457 ], [ -74.016619361218318, 49.735327510013313 ], [ -74.09841815282735, 49.735391377151736 ], [ -74.141625457503565, 49.679537938847204 ], [ -74.181497511497156, 49.679182617449257 ], [ -74.198506248540255, 49.610178317685119 ], [ -74.252650533795304, 49.567102528044757 ], [ -74.254526963323201, 49.511968346041272 ], [ -74.393706640652994, 49.474782579700467 ], [ -74.374935469729834, 49.442799013060011 ], [ -74.415701619343935, 49.382013009379605 ], [ -74.393395345015279, 49.369991140041499 ], [ -74.432853518167391, 49.323846801967193 ], [ -74.374842676398231, 49.316433385651678 ], [ -74.310702377359092, 49.258078990545066 ], [ -74.294565335895101, 49.289091523941501 ], [ -74.236496967878367, 49.287464736072124 ], [ -74.227217546918993, 49.217122300165983 ], [ -74.306763423124409, 49.041571620946748 ], [ -74.285232517440377, 48.990586158938406 ], [ -74.334945817545673, 48.966531324839508 ], [ -74.331658854131021, 48.922209945041651 ], [ -74.441019494041527, 48.932035376292021 ], [ -74.442051731341579, 48.975059211524616 ], [ -74.473349480843339, 48.946674928067694 ], [ -73.017742372481621, 47.998582582861786 ], [ -72.123637473327307, 47.998568367169206 ], [ -72.073470328859756, 47.949990039680458 ], [ -71.380006021783316, 47.950043356022327 ], [ -71.41162894502331, 47.99599292132477 ], [ -70.263649373981323, 47.992928196668693 ], [ -70.171875276252905, 48.124825950854877 ], [ -69.956658216509027, 48.057602006134921 ], [ -69.870213501943866, 48.181596546178383 ], [ -69.887263474875709, 48.220646027670114 ], [ -70.077875727198219, 48.252827776811912 ], [ -70.007992023269665, 48.357004317285998 ], [ -70.029889608212372, 52.83208781270055 ] ] ] } },
8 +{ "type": "Feature", "properties": { "code": "03", "name": "Capitale-Nationale", "geography_id": "region-03" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -69.668034142448676, 48.116365603295556 ], [ -69.870213501943866, 48.181596546178383 ], [ -69.956658216509027, 48.057602006134921 ], [ -70.171875276252905, 48.124825950854877 ], [ -70.263649373981323, 47.992928196668693 ], [ -71.277741933277042, 47.999357191155745 ], [ -71.41162894502331, 47.99599292132477 ], [ -71.380006021783316, 47.950043356022327 ], [ -72.073470328859756, 47.949990039680458 ], [ -71.890550235000347, 47.770184221377775 ], [ -72.239418820562292, 47.535906786493605 ], [ -72.141320196957963, 47.410545797658088 ], [ -72.238622306354003, 47.290993310959635 ], [ -72.413718343944282, 47.29258799319048 ], [ -72.415091480438079, 47.346849570661711 ], [ -72.462261013465167, 47.387327088007943 ], [ -72.541926270919177, 47.220052182777714 ], [ -72.514693969740335, 47.143985584961897 ], [ -72.378745467638637, 47.096670063023595 ], [ -72.435936882452737, 47.057484501448322 ], [ -72.167510331637416, 46.874580479659187 ], [ -72.393559845304253, 46.792105537821094 ], [ -72.059097502679919, 46.570065863675943 ], [ -71.872033345652156, 46.677398213824553 ], [ -71.690832336012477, 46.649841977385371 ], [ -71.359679261563258, 46.725868473983581 ], [ -71.23284067544931, 46.771256358346598 ], [ -71.170227220482928, 46.844262785262707 ], [ -71.000192026726708, 46.847477965797104 ], [ -70.842463384477369, 46.920060499421453 ], [ -70.706179022355244, 47.000893339345907 ], [ -70.674537858140056, 47.065671184499088 ], [ -70.524363351699876, 47.164836879234322 ], [ -70.454942106735729, 47.253504708328975 ], [ -70.288375919968885, 47.334062344776811 ], [ -70.14947993598517, 47.452950665186144 ], [ -70.069781222307483, 47.564698048148777 ], [ -69.910550122046899, 47.679365339581082 ], [ -69.719843658158908, 47.945531938820231 ], [ -69.548368964307301, 48.049451286329095 ], [ -69.668034142448676, 48.116365603295556 ] ] ] } },
9 +{ "type": "Feature", "properties": { "code": "04", "name": "Mauricie", "geography_id": "region-04" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -74.628528299078468, 48.967928210528704 ], [ -74.67627615106376, 49.000100609213888 ], [ -75.520552124226043, 49.000069842837618 ], [ -75.52108311081868, 47.762786669000775 ], [ -74.511510586554749, 47.762913212232824 ], [ -73.957427314645344, 47.38817074166036 ], [ -73.99897777547919, 47.345654068294458 ], [ -74.028582863686779, 47.177838740316375 ], [ -73.910582251261673, 47.099082725786459 ], [ -73.853251821247341, 47.151078007734448 ], [ -73.754786410209519, 47.066608391718958 ], [ -73.645947301680309, 46.937462374988222 ], [ -73.66420500507985, 46.928430977820284 ], [ -73.665448405545305, 46.863900769558484 ], [ -73.563991198189242, 46.883434344803256 ], [ -73.525943266818246, 46.862496996865673 ], [ -73.495076997383123, 46.912357008464134 ], [ -73.421237662478291, 46.890298277735525 ], [ -73.466958351044283, 46.709488617030409 ], [ -73.446013253223484, 46.59626519332074 ], [ -73.33650850439642, 46.601265189479548 ], [ -73.283001031209309, 46.533309347233555 ], [ -73.332946385296282, 46.498873757718741 ], [ -73.167002977363722, 46.382299797543638 ], [ -73.203427767519813, 46.358386264381274 ], [ -73.176173393606106, 46.323285879783676 ], [ -73.228451217800369, 46.316676010740366 ], [ -73.19567873073143, 46.294176916279092 ], [ -73.216691142333531, 46.279005524282347 ], [ -73.036707876104856, 46.156588056056776 ], [ -72.936245111859108, 46.151235214525087 ], [ -72.584675477647195, 46.292178045762704 ], [ -72.457086254740091, 46.38262139491821 ], [ -72.261829323494737, 46.436538240487778 ], [ -72.198908671019552, 46.531926948889009 ], [ -72.083010297404371, 46.57016485440311 ], [ -72.393559845304253, 46.792105537821094 ], [ -72.167510331637416, 46.874580479659187 ], [ -72.435936882452737, 47.057484501448322 ], [ -72.378745467638637, 47.096670063023595 ], [ -72.394376580293212, 47.117686726481814 ], [ -72.514693969740335, 47.143985584961897 ], [ -72.544151598119683, 47.19180318038002 ], [ -72.462261013465167, 47.387327088007943 ], [ -72.415091480438079, 47.346849570661711 ], [ -72.413718343944282, 47.29258799319048 ], [ -72.238622306354003, 47.290993310959635 ], [ -72.141320196957963, 47.410545797658088 ], [ -72.239418820562292, 47.535906786493605 ], [ -71.890550235000347, 47.770184221377775 ], [ -72.123637473327307, 47.998568367169206 ], [ -73.017742372481621, 47.998582582861786 ], [ -74.473349480843339, 48.946674928067694 ], [ -74.504776202898682, 48.92603866901505 ], [ -74.554259976518978, 49.000328406859218 ], [ -74.628528299078468, 48.967928210528704 ] ] ] } },
10 +{ "type": "Feature", "properties": { "code": "05", "name": "Estrie", "geography_id": "region-05" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -71.443021102978179, 45.23795889232062 ], [ -71.284224197807234, 45.302358900349063 ], [ -71.230614414673113, 45.249416647066532 ], [ -71.146258623733672, 45.239725536424437 ], [ -71.098200550576721, 45.30167331200979 ], [ -71.008520821205721, 45.319240839218878 ], [ -71.01065223453601, 45.347674995021393 ], [ -70.917835005885806, 45.311722477627193 ], [ -70.895048592277504, 45.240325275934111 ], [ -70.838065829639845, 45.234618352822359 ], [ -70.802532767212753, 45.365677211492255 ], [ -70.825736936115234, 45.400265287006043 ], [ -70.798094724949422, 45.426515839386354 ], [ -70.6587689063944, 45.377672496101148 ], [ -70.621269181747905, 45.40273637628222 ], [ -70.716395547558989, 45.486466965328582 ], [ -70.687692199657405, 45.56942196133901 ], [ -70.388425264321882, 45.750205814104 ], [ -70.421584258980573, 45.806879052362824 ], [ -70.47131985523535, 45.788828916834198 ], [ -70.499781722932013, 45.82643031702824 ], [ -70.631861631617895, 45.778682976592556 ], [ -70.651847606508028, 45.802721941836523 ], [ -70.895570053244185, 45.780116844051172 ], [ -70.898330992020945, 45.841799941594758 ], [ -71.029533777173654, 45.859131114369049 ], [ -71.094050537632825, 45.907066168491745 ], [ -71.070682307764542, 45.928350036604272 ], [ -71.129581244174034, 45.95733048468945 ], [ -71.162434229597238, 45.92344135188695 ], [ -71.135149628077386, 45.860284381604686 ], [ -71.178095409762548, 45.815105797795113 ], [ -71.348405226966108, 45.872239132696443 ], [ -71.340525510401875, 45.809642148014156 ], [ -71.392621357926842, 45.768845798672857 ], [ -71.462991932765746, 45.817438438612044 ], [ -71.541554867594229, 45.787721896821793 ], [ -71.617307455923466, 45.830459887264588 ], [ -71.635630367494372, 45.813886510663465 ], [ -71.770166310577153, 45.885008003950425 ], [ -71.861549478822056, 45.798949709074833 ], [ -71.921234294343748, 45.796312679190102 ], [ -72.029723388488179, 45.853250091340122 ], [ -72.206338858940342, 45.696387398495901 ], [ -72.22710020920654, 45.756215283087457 ], [ -72.311259830468998, 45.688733854358091 ], [ -72.237239174723115, 45.646239550115112 ], [ -72.297674910233312, 45.589406559848264 ], [ -72.359675278394604, 45.589394511454259 ], [ -72.402882465436264, 45.528484335743826 ], [ -72.404264784013307, 45.449202259034848 ], [ -72.454395190654509, 45.451382224352827 ], [ -72.448891019697939, 45.493590727210915 ], [ -72.586004737387029, 45.498289520969578 ], [ -72.604923587793806, 45.53768564860286 ], [ -72.799820629858019, 45.537984283382279 ], [ -72.883053652889188, 45.265547434664327 ], [ -72.989998687698574, 45.341932185325028 ], [ -73.04885686885568, 45.325351957532646 ], [ -73.015781549062282, 45.272101883006535 ], [ -73.046169602522852, 45.293109989836111 ], [ -73.063446871099544, 45.267768927457155 ], [ -73.058243389196775, 45.214581402871616 ], [ -73.091315679638683, 45.196142179396773 ], [ -73.139875247712169, 45.014416334322121 ], [ -71.501018913510123, 45.013443034828406 ], [ -71.498042806714636, 45.070189984928632 ], [ -71.428385248451946, 45.123672226499551 ], [ -71.39734167077053, 45.203518351667682 ], [ -71.443021102978179, 45.23795889232062 ] ] ] } },
11 +{ "type": "Feature", "properties": { "code": "06", "name": "Montréal", "geography_id": "region-06" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -73.474494241560407, 45.703765494606756 ], [ -73.614290233622512, 45.64182081843304 ], [ -73.677153236429973, 45.554934772678436 ], [ -73.763709133345159, 45.511898205271045 ], [ -73.910482022716167, 45.520967510403949 ], [ -73.99660217387256, 45.457487266119614 ], [ -73.982657099967184, 45.417792917830191 ], [ -73.822179339343961, 45.385403014947222 ], [ -73.76795484614351, 45.418680681935363 ], [ -73.545863425161329, 45.424292328833332 ], [ -73.474494241560407, 45.703765494606756 ] ] ] } },
12 +{ "type": "Feature", "properties": { "code": "07", "name": "Outaouais", "geography_id": "region-07" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -75.520649668590039, 47.847189404370802 ], [ -75.725316396732666, 47.847533344151287 ], [ -75.77210165831633, 47.818818355500142 ], [ -75.805795113177936, 47.669467960919746 ], [ -75.856904683891884, 47.713940396137609 ], [ -75.827843759251792, 47.736126383477732 ], [ -75.856909132628402, 47.803421880374593 ], [ -75.934512906304661, 47.80026845898724 ], [ -76.015805457815418, 47.723742829095542 ], [ -76.227954740037944, 47.730333474527789 ], [ -76.307171064751586, 47.666404778614464 ], [ -76.384934047759444, 47.661453930239134 ], [ -76.493523359906106, 47.599877247664956 ], [ -76.416197167851394, 47.603103040641393 ], [ -76.42481160855283, 47.512968278982449 ], [ -76.575405752175499, 47.604001334346137 ], [ -76.575717555967017, 47.289786837762072 ], [ -77.002046104590377, 47.290178101612298 ], [ -77.002270838943005, 47.145344686343073 ], [ -77.432656246217149, 47.144707761855813 ], [ -77.432627200072716, 47.290308437526271 ], [ -77.931267008686817, 47.269376741829973 ], [ -77.858372344834436, 47.236282103261551 ], [ -77.84732374585208, 47.105116252970639 ], [ -77.826173615235774, 47.108169922379176 ], [ -77.843739534204872, 46.871696684346404 ], [ -77.856077593522556, 46.941183091579852 ], [ -77.895178744813308, 46.829751437780544 ], [ -77.820625171075633, 46.821423607355484 ], [ -77.817485146928078, 46.709965233885789 ], [ -77.861799349392498, 46.656473727232637 ], [ -77.772493380443677, 46.582981577959401 ], [ -77.776024032172131, 46.453044392702949 ], [ -77.740112187730503, 46.403280843723067 ], [ -77.838786429695602, 46.314472483611688 ], [ -77.841600524610016, 46.206371388087945 ], [ -77.669067476440276, 46.195563928748854 ], [ -77.283709334575335, 46.018612437890987 ], [ -77.273582940883983, 45.944773152079058 ], [ -77.197351636207117, 45.870664884983306 ], [ -76.991527234922614, 45.786639421736126 ], [ -76.911949861650911, 45.809324495981315 ], [ -76.923102179018002, 45.89546026495676 ], [ -76.780624076468854, 45.873624957653043 ], [ -76.769280682695126, 45.733583209240635 ], [ -76.691860802368723, 45.710138717815909 ], [ -76.709411023178333, 45.664183192091855 ], [ -76.657611882567039, 45.559446200299334 ], [ -76.369006227956632, 45.458743052058473 ], [ -76.243647442951357, 45.467460710893619 ], [ -76.205687291592682, 45.51791989431397 ], [ -76.093475797384414, 45.517089313230137 ], [ -75.814671062500182, 45.373472303251738 ], [ -75.681101985068196, 45.459427058760845 ], [ -75.238940780018808, 45.586437633604014 ], [ -75.123420274207348, 45.577428202760629 ], [ -74.938089018413464, 45.644966062432971 ], [ -74.807766485218835, 45.638317997372383 ], [ -74.698759978142306, 45.971776245459942 ], [ -74.801302826693032, 45.94509613192642 ], [ -74.858617777396333, 46.060743378305773 ], [ -74.939450909792754, 46.061728800812439 ], [ -74.939544277084366, 46.105533128782604 ], [ -75.089790696686322, 46.103131465914828 ], [ -75.074034858220458, 46.215292787572253 ], [ -75.117388337025787, 46.215204782338553 ], [ -75.176360583907524, 46.103409070907723 ], [ -75.407157497500535, 46.097862133633278 ], [ -75.40720978574069, 45.972950674008828 ], [ -75.599159410171552, 45.971449813692011 ], [ -75.620766718698903, 45.99471255220395 ], [ -75.784749726565195, 45.959745395087303 ], [ -75.697274975935656, 46.023889317706846 ], [ -75.764419246762529, 46.022219814433356 ], [ -75.768311307229538, 46.17417628912839 ], [ -75.79901348555326, 46.174121622782152 ], [ -75.813144849910231, 46.224916684772147 ], [ -75.783033258740502, 46.282010335183763 ], [ -75.814570296980875, 46.306351082622271 ], [ -75.707042202236863, 46.309165822329106 ], [ -75.706004087524875, 46.438816366965149 ], [ -75.745213974613961, 46.438256989474063 ], [ -75.746957404129944, 46.844824405657036 ], [ -75.890005118585236, 46.815501543624606 ], [ -75.951767366861986, 46.869493132957807 ], [ -76.137221267298031, 46.937268542227528 ], [ -76.148472638218152, 47.006168967013544 ], [ -76.102355610337128, 47.03047835950585 ], [ -76.094056880560345, 47.187733711472099 ], [ -75.976184969153806, 47.203159978619652 ], [ -75.803433606163409, 47.318536358414676 ], [ -75.747833523399976, 47.272918806008541 ], [ -75.647836725372869, 47.281748369106872 ], [ -75.625835534409759, 47.180936354364015 ], [ -75.473452526701323, 47.282386261067401 ], [ -75.46043712927208, 47.367888553088342 ], [ -75.308027261425593, 47.547913465039748 ], [ -75.360287105443618, 47.570213293959497 ], [ -75.36959914065028, 47.627142395229775 ], [ -75.41419349614057, 47.588627982766532 ], [ -75.446512536589253, 47.663614582766868 ], [ -75.524603928751503, 47.710975082476388 ], [ -75.520649668590039, 47.847189404370802 ] ] ] } },
13 +{ "type": "Feature", "properties": { "code": "08", "name": "Abitibi-Témiscamingue", "geography_id": "region-08" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -77.84167815336879, 46.29199934848797 ], [ -77.740112187730503, 46.403280843723067 ], [ -77.776024032172131, 46.453044392702949 ], [ -77.772493380443677, 46.582981577959401 ], [ -77.861799349392498, 46.656473727232637 ], [ -77.817485146928078, 46.709965233885789 ], [ -77.820625171075633, 46.821423607355484 ], [ -77.848309870090262, 46.812167251765956 ], [ -77.895178744813308, 46.829751437780544 ], [ -77.856077593522556, 46.941183091579852 ], [ -77.845399655286656, 46.928642276596769 ], [ -77.850796967610847, 46.891549662852825 ], [ -77.849458980665034, 46.872214142901427 ], [ -77.843739534204872, 46.871696684346404 ], [ -77.826173615235774, 47.108169922379176 ], [ -77.842906589810127, 47.10341390816766 ], [ -77.84732374585208, 47.105116252970639 ], [ -77.858372344834436, 47.236282103261551 ], [ -77.931267008686817, 47.269376741829973 ], [ -77.432627200072716, 47.290308437526271 ], [ -77.432656246217149, 47.144707761855813 ], [ -77.002270838943005, 47.145344686343073 ], [ -77.002046104590377, 47.290178101612298 ], [ -76.575717555967017, 47.289786837762072 ], [ -76.575405752175499, 47.604001334346137 ], [ -76.42481160855283, 47.512968278982449 ], [ -76.416197167851394, 47.603103040641393 ], [ -76.42230858977571, 47.607541906719469 ], [ -76.444077843274727, 47.599315059308914 ], [ -76.451452618191809, 47.604154834963936 ], [ -76.461538996894973, 47.599870966203 ], [ -76.48281658967268, 47.602862488108208 ], [ -76.484227085079922, 47.600497000194075 ], [ -76.492220349073378, 47.597889245285252 ], [ -76.493523359906106, 47.599877247664956 ], [ -76.384934047759444, 47.661453930239134 ], [ -76.307171064751586, 47.666404778614464 ], [ -76.227954740037944, 47.730333474527789 ], [ -76.015805457815418, 47.723742829095542 ], [ -75.934512906304661, 47.80026845898724 ], [ -75.856909132628402, 47.803421880374593 ], [ -75.827843759251792, 47.736126383477732 ], [ -75.856904683891884, 47.713940396137609 ], [ -75.805795113177936, 47.669467960919746 ], [ -75.77210165831633, 47.818818355500142 ], [ -75.725316396732666, 47.847533344151287 ], [ -75.520642706984191, 47.84773784003152 ], [ -75.520552124226043, 49.000069842837618 ], [ -79.517798777935582, 49.009119745036998 ], [ -79.511580065343409, 47.540755593117893 ], [ -79.555888627767843, 47.515978856662336 ], [ -79.579333918908006, 47.432643008902822 ], [ -79.4342424365082, 47.258887228348748 ], [ -79.432660242856826, 47.080553291583406 ], [ -79.22728698878133, 46.845269172940291 ], [ -79.166863369085988, 46.821045569398933 ], [ -79.093776408409411, 46.688349726743994 ], [ -79.015610321917634, 46.631460025549899 ], [ -78.987860035328865, 46.547014121462858 ], [ -78.889633970277515, 46.460124155971592 ], [ -78.729294744359493, 46.384818044199754 ], [ -78.711266873459977, 46.324984726613728 ], [ -78.394840352724515, 46.292540003576875 ], [ -78.308615340496871, 46.253178567707621 ], [ -78.146983660197435, 46.276901439123463 ], [ -77.841600524610016, 46.206371388087945 ], [ -77.84167815336879, 46.29199934848797 ] ] ] } },
14 +{ "type": "Feature", "properties": { "code": "09", "name": "Côte-Nord", "geography_id": "region-09" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -57.107825288920488, 51.806053933015811 ], [ -57.11861639612836, 51.198882209428426 ], [ -57.736140913255717, 50.998608066974619 ], [ -58.940366075568242, 49.848609707635632 ], [ -60.4768183638529, 48.781392206912379 ], [ -60.468597199247426, 48.666667945248911 ], [ -62.999128701777302, 48.666683203520115 ], [ -62.999128389281516, 48.701792945343961 ], [ -63.526312062315604, 48.929185506696911 ], [ -64.163776728351948, 49.319940799915813 ], [ -64.595652328655106, 49.509721780934925 ], [ -65.178200944840341, 49.668475703752648 ], [ -65.818586606783697, 49.717034520407651 ], [ -66.078485917005565, 49.692702208684359 ], [ -66.376998539123647, 49.619091764423644 ], [ -66.595258093378234, 49.52167529709115 ], [ -67.153339142634252, 49.18906403276106 ], [ -67.653527346887046, 49.059287454794955 ], [ -68.403560660051355, 48.787038498057406 ], [ -68.999999195820209, 48.451724286567014 ], [ -69.548368964307301, 48.049451286329095 ], [ -69.676673434722431, 48.119504527453621 ], [ -69.840734878094736, 48.152784095925362 ], [ -69.893624803044318, 48.225785046233547 ], [ -70.077875727198219, 48.252827776811912 ], [ -70.007992023269665, 48.357004317285998 ], [ -70.029882586478294, 55.000135978323222 ], [ -67.396103909968772, 54.989968577548034 ], [ -67.253073063711994, 54.833854671596548 ], [ -67.20840453378392, 54.824520869720807 ], [ -67.207155075651244, 54.790659146604654 ], [ -67.053178025612809, 54.695712206771361 ], [ -67.133960852620106, 54.622016945873568 ], [ -67.278774138039324, 54.587794989803356 ], [ -67.291025348193045, 54.552905840144852 ], [ -67.219050522831679, 54.520821404158553 ], [ -67.26283075092698, 54.483487746351649 ], [ -67.492755297848674, 54.581686393448138 ], [ -67.519062229233697, 54.569908359622495 ], [ -67.471116363136062, 54.531934744328147 ], [ -67.492090623636315, 54.47982220414292 ], [ -67.623040585146583, 54.473961977489282 ], [ -67.645596448744385, 54.508351372783665 ], [ -67.669959524630329, 54.446322764089892 ], [ -67.774714058654183, 54.43843445822084 ], [ -67.714018010369642, 54.388127760961794 ], [ -67.740880830315334, 54.347127177995517 ], [ -67.662906158555586, 54.311848311003011 ], [ -67.670351923850106, 54.240291651330274 ], [ -67.606516460746448, 54.207262999730425 ], [ -67.651240799927137, 54.186568616571492 ], [ -67.635740595181304, 54.158762499877241 ], [ -67.803637272244188, 54.130013886339242 ], [ -67.750275579812552, 54.088877760739827 ], [ -67.817612439631503, 54.023931958499759 ], [ -67.676468261143242, 53.97854164617933 ], [ -67.666579151605475, 53.937066079056187 ], [ -67.583158882509665, 53.923286396379972 ], [ -67.603299477702564, 53.873091348410412 ], [ -67.488850814767616, 53.838534681421805 ], [ -67.593023410855466, 53.793479468404499 ], [ -67.545441863756309, 53.777841887403092 ], [ -67.593941299400356, 53.764034694732686 ], [ -67.492059451767091, 53.756345254059596 ], [ -67.513310876192293, 53.734956330912283 ], [ -67.469113876417595, 53.714927563360135 ], [ -67.406131918408192, 53.721868315163192 ], [ -67.383058641753323, 53.616732490417583 ], [ -67.303694434854762, 53.587648336087007 ], [ -67.307555775238967, 53.552369730320464 ], [ -67.03637536457795, 53.536005015963696 ], [ -66.971181133005516, 53.500005539743171 ], [ -66.989932701426625, 53.468671893313498 ], [ -66.932235799236452, 53.475616073585456 ], [ -66.921459135097166, 53.425309725393063 ], [ -66.875178943833731, 53.413642192147492 ], [ -67.020575332209958, 53.339531403096473 ], [ -66.944547547945831, 53.283280565224167 ], [ -66.974467829070392, 53.22875276334431 ], [ -66.951190804164526, 53.180446652219636 ], [ -67.006221941393235, 53.156614156597001 ], [ -66.942275792913961, 53.137112531257401 ], [ -66.983639676314382, 53.118640840665698 ], [ -66.978141608102518, 53.083834463516652 ], [ -67.072335057785935, 53.073418079704503 ], [ -67.055807811150061, 53.137364145003026 ], [ -67.115942967423464, 53.100305809579325 ], [ -67.144751658265406, 53.140307263933792 ], [ -67.267650855670482, 53.177032802070322 ], [ -67.286283911272662, 53.138862756735818 ], [ -67.372387221550042, 53.101635531312034 ], [ -67.344189712650959, 52.982281440415903 ], [ -67.232796917051459, 52.981781953336686 ], [ -67.338084785901472, 52.951837518871514 ], [ -67.341368378914609, 52.894170276016936 ], [ -67.165828067379664, 52.815249705403652 ], [ -67.150963575445573, 52.85344500844738 ], [ -67.06367060257233, 52.875690833424535 ], [ -67.07037858324702, 52.776942730864079 ], [ -67.032377244907593, 52.749136621413165 ], [ -66.955628392884108, 52.760412201768602 ], [ -66.899941416216151, 52.678465285462828 ], [ -66.853300286697987, 52.674103863430119 ], [ -66.896438928307489, 52.730771646773647 ], [ -66.855740044651554, 52.719799458239606 ], [ -66.858677746136536, 52.768855530921996 ], [ -66.760753904074221, 52.667265864143261 ], [ -66.752423297435726, 52.745744164037404 ], [ -66.789338594804349, 52.799467245431778 ], [ -66.714838018332486, 52.759910811970911 ], [ -66.647190829350592, 52.782270575052294 ], [ -66.665080479001162, 52.892438873529201 ], [ -66.631466992586255, 52.898716337534822 ], [ -66.630742711944222, 52.955911406248738 ], [ -66.497915022224902, 52.948184748414505 ], [ -66.460286453374422, 53.052743598313221 ], [ -66.36574220917224, 53.020601647235537 ], [ -66.257606689881044, 52.866793302468139 ], [ -66.294915601934647, 52.860238002342221 ], [ -66.286172175369771, 52.835904682869533 ], [ -66.387054188688836, 52.877793895211383 ], [ -66.417481957335056, 52.850627835387463 ], [ -66.386068916438632, 52.795738333024907 ], [ -66.346537797116682, 52.794432521328858 ], [ -66.307726590070956, 52.725764490410775 ], [ -66.339492213968981, 52.683401652730581 ], [ -66.274391984778191, 52.626957215007216 ], [ -66.316242835033336, 52.609206130902919 ], [ -66.387855293607643, 52.681790871004011 ], [ -66.440904514067498, 52.637653909390181 ], [ -66.380112017636861, 52.565987442403348 ], [ -66.371716682872503, 52.531069678447793 ], [ -66.400963027784698, 52.531961335801526 ], [ -66.354370077744235, 52.469928904895013 ], [ -66.393708366170856, 52.475099964479853 ], [ -66.339886897873413, 52.366592773173466 ], [ -66.361989418947587, 52.350429759922505 ], [ -66.415729428000134, 52.386355786286927 ], [ -66.492566680241424, 52.340772258515962 ], [ -66.432077175361002, 52.265803069056538 ], [ -66.470159136410459, 52.253831629324431 ], [ -66.359373686096177, 52.179267458475245 ], [ -66.374404940744995, 52.150016412050661 ], [ -66.269689115895304, 52.148650269198015 ], [ -66.314564499801676, 52.291263877638741 ], [ -66.2563875824126, 52.310149718515127 ], [ -66.199268881320307, 52.224182768643864 ], [ -66.097612782170941, 52.200563036698163 ], [ -66.070008540389054, 52.149201426274431 ], [ -66.094343583730492, 52.088951110076465 ], [ -65.978281569704478, 52.053116390102694 ], [ -65.972088670677451, 52.108894984199665 ], [ -65.89808410378572, 52.077532735367164 ], [ -65.871556393543131, 52.120310844893829 ], [ -65.802692792107777, 52.124976999333882 ], [ -65.732941731388138, 52.083115582621332 ], [ -65.666080562017953, 52.121728060795441 ], [ -65.640131178189037, 52.060283044617243 ], [ -65.674362130862647, 52.012645290599629 ], [ -65.647166990620732, 51.990950230469728 ], [ -65.620988830375026, 52.048088050517805 ], [ -65.538564144719359, 52.04539164280397 ], [ -65.453965585788822, 52.161311672839219 ], [ -65.485022172317557, 52.175756360910619 ], [ -65.469910471121239, 52.191534147655453 ], [ -65.249533129429423, 52.281556970315158 ], [ -65.199562817863082, 52.261808096037022 ], [ -65.208310528626512, 52.201055827907751 ], [ -65.170253291246382, 52.195305542789995 ], [ -65.146614911371216, 52.10458221702266 ], [ -65.087000541348772, 52.166804663445191 ], [ -64.956702171812651, 52.105815567774222 ], [ -64.906127515336152, 52.045297209505812 ], [ -64.825430352501527, 52.0283799757418 ], [ -64.806006707046421, 52.004456086578003 ], [ -64.841986643106168, 51.976379170637173 ], [ -64.704186962844034, 51.87866857496001 ], [ -64.780114451817994, 51.780176090582124 ], [ -64.696887274617765, 51.75464833386431 ], [ -64.673496135176407, 51.701146374501754 ], [ -64.632605250616436, 51.70623001222706 ], [ -64.620965360505821, 51.632974466661658 ], [ -64.553153354930899, 51.577216949315591 ], [ -64.449563121954654, 51.629615564776294 ], [ -64.426867466342927, 51.681918906979639 ], [ -64.340200300439648, 51.665602233132432 ], [ -64.278554231050961, 51.737424740898398 ], [ -64.307555090034612, 51.75592612995262 ], [ -64.296942753273541, 51.79845446054216 ], [ -64.357557546833505, 51.848141373350444 ], [ -64.363974995689574, 51.980965317358255 ], [ -64.341279463288345, 51.986687545210543 ], [ -64.365863651013726, 52.006215534302257 ], [ -64.234159407070152, 51.975440284337843 ], [ -64.296207151322406, 52.082113344051727 ], [ -64.250937975989132, 52.082790565237005 ], [ -64.254077222336889, 52.149234975168014 ], [ -64.165822513966702, 52.120346652183912 ], [ -64.254408912030968, 52.277469421524792 ], [ -64.208575633218189, 52.284995803972151 ], [ -64.22252023883118, 52.315357213429351 ], [ -64.16635143898047, 52.30985696783673 ], [ -64.190490594015401, 52.334301910933711 ], [ -64.106645786935715, 52.409126975656221 ], [ -64.136923048499767, 52.430628066479514 ], [ -64.149170889782255, 52.517602201607495 ], [ -64.232481099086286, 52.583170241231407 ], [ -64.158624714914154, 52.606319756820618 ], [ -64.175846107612486, 52.672194743261102 ], [ -64.124038403119613, 52.695278065627775 ], [ -64.148760834686584, 52.730028631394418 ], [ -63.937669410984739, 52.730471633757332 ], [ -63.874055349655585, 52.776083016019001 ], [ -63.722771318847435, 52.773221144090137 ], [ -63.719854482276475, 52.802054754306901 ], [ -63.637572767774955, 52.833138025555151 ], [ -63.682102297751754, 52.850583067824523 ], [ -63.613515331869316, 52.839526623683355 ], [ -63.602970014241066, 52.91871912509535 ], [ -63.507960018390172, 52.956523831167317 ], [ -63.446846743594023, 52.944884429676868 ], [ -63.480987759648428, 52.90452276445918 ], [ -63.460879179171826, 52.881550850386574 ], [ -63.39674588552932, 52.886940812655894 ], [ -63.366671343911413, 52.848941984054854 ], [ -63.388451404567633, 52.837469697203844 ], [ -63.318060848657247, 52.821913598201448 ], [ -63.288893308327566, 52.78071858394101 ], [ -63.315811374368415, 52.769246372943272 ], [ -63.242781339013334, 52.720439727452089 ], [ -63.154721006924873, 52.817440561146391 ], [ -62.965325331241836, 52.75863038753787 ], [ -62.933628889499239, 52.794963851497116 ], [ -62.667008645000131, 52.77574244029514 ], [ -62.694481029840325, 52.813270837294894 ], [ -62.636951593857269, 52.796353592545636 ], [ -62.604505518131326, 52.822548375367177 ], [ -62.704171077288741, 52.863688047148479 ], [ -62.66808966951249, 52.886910518497146 ], [ -62.720830012526967, 52.8972435863977 ], [ -62.72221449861626, 52.923465790499009 ], [ -62.641086992454795, 52.935438853768943 ], [ -62.585725744317081, 52.894660248164961 ], [ -62.528543343275771, 52.915493615937613 ], [ -62.44415717859102, 52.865381127562891 ], [ -62.508306718851117, 52.835575604540701 ], [ -62.468806138491722, 52.800880525997933 ], [ -62.485890228675494, 52.788963918728612 ], [ -62.370719966182435, 52.745879132988783 ], [ -62.177184987561766, 52.73607250529966 ], [ -62.135457976706199, 52.777350246350011 ], [ -61.907682731598001, 52.695763571195037 ], [ -61.861270610265819, 52.632790267462163 ], [ -61.744594197498799, 52.609343861797711 ], [ -61.714815256009331, 52.57400966969373 ], [ -61.462378854774784, 52.532536914281842 ], [ -61.453217802158768, 52.471148083544989 ], [ -61.215768792237775, 52.508003671289096 ], [ -61.056918635036538, 52.443421344471282 ], [ -61.003758047807452, 52.385894149096544 ], [ -60.653774550780099, 52.303668876555875 ], [ -60.563104213571656, 52.319585034803097 ], [ -60.604076709561191, 52.397752807356916 ], [ -60.517377752649821, 52.454502743266048 ], [ -60.22687978762319, 52.4525283547149 ], [ -60.080944149637418, 52.518000512241841 ], [ -59.967359128528173, 52.496193868942434 ], [ -60.000748620964579, 52.470776366346158 ], [ -59.982341377601045, 52.451358923981317 ], [ -59.843547729207856, 52.447720245674638 ], [ -59.834880623524136, 52.467887233920528 ], [ -60.015829485207661, 52.570611952436309 ], [ -59.815544791299963, 52.562943608042708 ], [ -59.842461362321465, 52.608638933545564 ], [ -59.905658051366117, 52.620194966076618 ], [ -59.798653081297694, 52.678389153933388 ], [ -59.709705249914407, 52.676277519557502 ], [ -59.539479227245565, 52.556969436741703 ], [ -59.498948597181574, 52.621691943632612 ], [ -59.384666937152708, 52.606218891221708 ], [ -59.309302595333286, 52.630190857490312 ], [ -59.242496075910424, 52.554661716958556 ], [ -59.141465525626614, 52.516799422082627 ], [ -59.049850486624315, 52.54096557110379 ], [ -58.921041584410105, 52.473213856144717 ], [ -58.859817174918078, 52.464713303132086 ], [ -58.831871125267355, 52.504991427293803 ], [ -58.650467464653488, 52.457660046976379 ], [ -58.622160295566559, 52.474076985996419 ], [ -58.667497476837788, 52.49496529104929 ], [ -58.678857857086967, 52.559993653605581 ], [ -58.521522523280126, 52.575713070171084 ], [ -58.572091957965235, 52.651655304289726 ], [ -58.447978574850886, 52.654122180888464 ], [ -58.455617165837914, 52.696400843302357 ], [ -58.384194694342284, 52.708648883865457 ], [ -58.218707531525297, 52.62826582970586 ], [ -58.257014153980528, 52.611182169075157 ], [ -58.125842113735139, 52.562457770380384 ], [ -58.083868529641094, 52.57442973385173 ], [ -58.063106548216901, 52.490450575204342 ], [ -57.974685523625105, 52.42655883279987 ], [ -57.860874250420139, 52.412751409402183 ], [ -57.868346317743594, 52.441779399389866 ], [ -57.809649775651103, 52.468501611643745 ], [ -57.848123306926134, 52.499724465101735 ], [ -57.815038353400382, 52.511641076162043 ], [ -57.731900892437437, 52.478669418035992 ], [ -57.786509986179155, 52.427806358483686 ], [ -57.748592205955582, 52.418500580748002 ], [ -57.739882572188705, 52.370727838894979 ], [ -57.676778370381783, 52.31628579543154 ], [ -57.588109193975917, 52.288868051319199 ], [ -57.467801493507579, 52.189616092521419 ], [ -57.409119646082125, 52.066076390374448 ], [ -57.333534223958161, 52.052075869014025 ], [ -57.278503082629172, 52.096575795463814 ], [ -57.166977519718273, 52.057940258916886 ], [ -57.076274480766358, 52.103354424666698 ], [ -56.989244927930088, 52.07771500278885 ], [ -56.935204453191488, 51.973276106105992 ], [ -57.022647555092327, 51.992302778909298 ], [ -57.051459542388109, 51.919859713836829 ], [ -57.081127496221633, 51.915610310973307 ], [ -57.056544121284638, 51.872276116368312 ], [ -57.107825288920488, 51.806053933015811 ] ] ], [ [ [ -63.564797186527642, 54.987862753823713 ], [ -63.614578622140087, 54.976751933186257 ], [ -63.578631272123509, 54.951973329842822 ], [ -63.606968312586574, 54.943862176035104 ], [ -63.578047696868659, 54.926472529551191 ], [ -63.628362154992722, 54.924584129487179 ], [ -63.604386093072826, 54.899333342124827 ], [ -63.828071414044651, 54.945002527972179 ], [ -63.790569942805057, 54.930585271753117 ], [ -63.857379192478696, 54.886918314745948 ], [ -63.811128625915373, 54.818250551956538 ], [ -63.926598571450107, 54.778638933550774 ], [ -63.782462735107316, 54.715470876758296 ], [ -63.709757206237789, 54.622301929584864 ], [ -63.814073678700424, 54.648636706348753 ], [ -63.894261404097897, 54.598691646991348 ], [ -63.963902478485025, 54.618108845275351 ], [ -64.0522202274731, 54.597303879822846 ], [ -64.184145777637212, 54.692500826538428 ], [ -64.185367490708941, 54.722834389773553 ], [ -64.28289925086473, 54.728640821729208 ], [ -64.297259755048728, 54.763724689624496 ], [ -64.39401359718984, 54.788225789565402 ], [ -64.48734393313002, 54.793392465707363 ], [ -64.477623604135772, 54.746391639354265 ], [ -64.550401615764656, 54.762697816002451 ], [ -64.528070066737953, 54.72953077250812 ], [ -64.552461901652109, 54.710558320144301 ], [ -64.788388940392096, 54.731171413617496 ], [ -64.803388390818995, 54.75872752735124 ], [ -64.751472849577951, 54.767227473972113 ], [ -64.780694728255384, 54.787033576339674 ], [ -64.747610822920848, 54.808978322030285 ], [ -64.772333539834094, 54.837756625360015 ], [ -64.86644826751089, 54.841229450677503 ], [ -64.859474960839762, 54.886202245100463 ], [ -65.077538050593134, 54.967316094731082 ], [ -65.128040749629434, 54.924926922715514 ], [ -65.174098293367138, 54.929454972165971 ], [ -65.198572724194307, 54.849092752658848 ], [ -65.288993632124829, 54.839898641524883 ], [ -65.297771312556108, 54.812565292813439 ], [ -65.329300505801612, 54.844482255622722 ], [ -65.353856212523553, 54.817371713547153 ], [ -65.482802779605649, 54.832734139197377 ], [ -65.431274354991373, 54.781372814388291 ], [ -65.451026090788162, 54.732122172975416 ], [ -65.647506042268063, 54.751013051917433 ], [ -65.679535558209253, 54.705262543542077 ], [ -65.855069155246056, 54.851291151455889 ], [ -65.852262834526996, 54.926598088749877 ], [ -66.026909113235121, 54.942709714770039 ], [ -66.036436935791983, 54.916709417179895 ], [ -66.159497765328481, 54.988516075927976 ], [ -66.243306099676843, 54.971766693374398 ], [ -66.259807378616216, 54.998683929520993 ], [ -63.564797186527642, 54.987862753823713 ] ] ], [ [ [ -66.750048101000232, 55.000103614678743 ], [ -66.58582499861852, 54.812599139348855 ], [ -66.652882272507, 54.76634944504714 ], [ -66.748356944809615, 54.813128577798501 ], [ -66.667188839115653, 54.7123763698104 ], [ -66.873695479796041, 54.812934671364367 ], [ -66.935142852550598, 54.79971279227356 ], [ -67.224234775077619, 54.995440241234363 ], [ -66.750048101000232, 55.000103614678743 ] ] ], [ [ [ -66.643240784157186, 55.000102216056803 ], [ -66.645324707630778, 54.976601394084447 ], [ -66.687658339330468, 55.000130408334478 ], [ -66.643240784157186, 55.000102216056803 ] ] ] ] } },
15 +{ "type": "Feature", "properties": { "code": "10", "name": "Nord-du-Québec", "geography_id": "region-10" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -77.499999959000888, 62.57944802246044 ], [ -77.721934006547684, 62.468803909549209 ], [ -78.090066892309636, 62.362655707424011 ], [ -78.200405133951548, 62.255267975668673 ], [ -78.165157752090991, 62.220007918415568 ], [ -78.185933757678811, 62.148322721399794 ], [ -78.162965298677136, 61.997596404916244 ], [ -78.092302762719953, 61.928833906606734 ], [ -78.101024989644031, 61.857196908020455 ], [ -78.058912664345556, 61.827907219114991 ], [ -77.997803488162759, 61.681452590522412 ], [ -77.888860223274577, 61.638144593535976 ], [ -77.806798676697028, 61.466727097741305 ], [ -77.773387772316866, 61.280113812880323 ], [ -77.743566398765395, 61.259409488100268 ], [ -77.793496040768105, 61.240444219672845 ], [ -77.832039119396498, 61.057672093264806 ], [ -78.09694517650216, 60.930063688129437 ], [ -78.194432007666336, 60.85832791729478 ], [ -78.227856853635274, 60.789295488121759 ], [ -78.078766872281633, 60.792029462772149 ], [ -77.845238814247736, 60.660283688283762 ], [ -77.912314516043452, 60.603562803734413 ], [ -77.905789157378209, 60.570114904441695 ], [ -77.738564603477357, 60.452961208355475 ], [ -77.775557873420368, 60.385491414097402 ], [ -77.607849579201869, 60.291883508731779 ], [ -77.63542987515001, 60.253438498084918 ], [ -77.569706606041152, 60.188349103728306 ], [ -77.625329001769828, 60.117502020079755 ], [ -77.607620390034015, 60.097270793079886 ], [ -77.674605368811214, 60.065861783891442 ], [ -77.465229127183804, 59.917033415981351 ], [ -77.464709485555957, 59.793951579123629 ], [ -77.539457429967172, 59.756387792880496 ], [ -77.752960790550162, 59.762856326298149 ], [ -77.846911772174863, 59.726619014613199 ], [ -77.879439259719192, 59.624386194334491 ], [ -77.835797614426326, 59.601587092034748 ], [ -77.846746227942674, 59.5530684126837 ], [ -77.909401470765943, 59.506903380187993 ], [ -77.989230200547198, 59.261993209276611 ], [ -78.189933422745526, 59.19436699196099 ], [ -78.165379785845715, 59.185927575932567 ], [ -78.298916117448655, 59.055710892924957 ], [ -78.555400666098322, 58.948879814787091 ], [ -78.584554226829056, 58.904787488025491 ], [ -78.557717094056912, 58.842514394399714 ], [ -78.631422714602138, 58.812915582820132 ], [ -78.538709005670796, 58.750276796291786 ], [ -78.592684594311422, 58.650078701622249 ], [ -78.487940669787719, 58.617645006438252 ], [ -78.481786523010584, 58.576456481883653 ], [ -78.426546675051853, 58.541350181841054 ], [ -78.240964532916493, 58.494268611723257 ], [ -78.226039956389627, 58.45419058236827 ], [ -78.254813936187844, 58.456600999315185 ], [ -78.205308929717035, 58.402149888047589 ], [ -77.650330117719719, 58.263942804165929 ], [ -77.526906772211774, 58.160344389739173 ], [ -77.220430598716064, 58.025494877297582 ], [ -77.241740141733032, 58.014902809467088 ], [ -77.089977369611034, 57.929838625005999 ], [ -76.922562516511661, 57.75562790209856 ], [ -76.760522721257757, 57.460580476563379 ], [ -76.675322680729323, 57.399127310306042 ], [ -76.586707536401491, 57.188637611600676 ], [ -76.534927315269897, 56.8171426883385 ], [ -76.559969077393404, 56.556625494426385 ], [ -76.552868530599468, 56.477319279487944 ], [ -76.520704976190885, 56.458523321453768 ], [ -76.529605515598675, 56.379326210174547 ], [ -76.559469906641453, 56.349582877915729 ], [ -76.549152308194039, 56.301263219610483 ], [ -76.626593378701585, 56.238378117936826 ], [ -76.61758780248276, 56.20029158986258 ], [ -76.670697877846507, 56.193808098944771 ], [ -76.64424361943918, 56.146793291134955 ], [ -76.709082235364008, 56.034155593246581 ], [ -76.800330391394908, 56.000877979572508 ], [ -76.830186884701405, 55.929854509328734 ], [ -76.875799338456432, 55.923453813622928 ], [ -77.192991895599675, 55.680408898291951 ], [ -77.176433726851556, 55.644160777560728 ], [ -77.2045456970217, 55.616714003742388 ], [ -77.556753209842299, 55.398558285160036 ], [ -77.726855513406534, 55.333094791767486 ], [ -77.790782581487434, 55.268722085919002 ], [ -78.197962699302352, 55.120791815886598 ], [ -78.316586611119646, 55.042729108572253 ], [ -78.891965111672363, 54.89745851972895 ], [ -78.981927185654072, 54.833569078321595 ], [ -79.448031800710396, 54.760493315963473 ], [ -79.763296395135583, 54.632494418388582 ], [ -79.68561072610575, 54.618414017328881 ], [ -79.612705424105258, 54.557433803690309 ], [ -79.637236919276631, 54.551067217385196 ], [ -79.632354232647671, 54.547485394469703 ], [ -79.533880967438989, 54.519072114025661 ], [ -79.55774842402792, 54.47431288313058 ], [ -79.53631467427482, 54.467017008697603 ], [ -79.596457510175, 54.465919379999569 ], [ -79.535327926233307, 54.463218807472003 ], [ -79.506233072739732, 54.41354922274445 ], [ -79.512155435468586, 54.363658196078447 ], [ -79.471334186192962, 54.326685011741574 ], [ -79.49609868337653, 54.264852718242444 ], [ -79.478184836678309, 54.221592814133537 ], [ -79.394869313406488, 54.176372513766822 ], [ -79.110506689830416, 53.907108507166654 ], [ -79.142779832380342, 53.901030516338551 ], [ -79.080914427599069, 53.870975803273254 ], [ -79.1160808956501, 53.831810129761749 ], [ -79.089579787970081, 53.828643321293583 ], [ -79.083506974292291, 53.757595881219295 ], [ -79.155601270380245, 53.70486711477119 ], [ -79.055370500641459, 53.606187093736715 ], [ -79.143137708999248, 53.491082082172724 ], [ -79.065417130161478, 53.407765704726032 ], [ -78.949907514090384, 53.379146296465329 ], [ -79.010309281833884, 53.29629239656385 ], [ -79.054549440032332, 53.292343815119473 ], [ -79.013128298252738, 53.288953389625256 ], [ -78.93896422639709, 53.211375644052069 ], [ -78.979218179279457, 53.161381796897047 ], [ -78.947402362432555, 53.13798108712129 ], [ -78.999561622203046, 53.092359579887237 ], [ -78.956758581637359, 52.94518121101801 ], [ -79.012827281412953, 52.904833398430164 ], [ -78.953670773980264, 52.940135720750952 ], [ -78.899876504295023, 52.899250784614551 ], [ -78.872870825264513, 52.740263184916806 ], [ -78.827573969033978, 52.699525698745923 ], [ -78.822142121935755, 52.640779613173159 ], [ -78.778857178099457, 52.622168917761975 ], [ -78.768673711549383, 52.541715895689315 ], [ -78.56913850050023, 52.5015776983333 ], [ -78.584076775585416, 52.395957208211975 ], [ -78.516929700701652, 52.332109722244681 ], [ -78.58882760567846, 52.242918378544893 ], [ -78.550510797117539, 52.188642478265699 ], [ -78.596202565395529, 52.108563698260262 ], [ -78.666323029897086, 52.054001196090347 ], [ -78.721840732648332, 52.052876902314772 ], [ -78.702602371333413, 52.02889582268859 ], [ -78.801051189405527, 51.958551621926944 ], [ -78.887553983680419, 51.957225010894533 ], [ -78.969653194174299, 51.799719699377114 ], [ -79.051193070631442, 51.764191097388149 ], [ -78.941093409038231, 51.692968793204123 ], [ -78.927974432528302, 51.64534710686879 ], [ -78.850046611566242, 51.616373796656532 ], [ -78.812368144932364, 51.454852903504523 ], [ -78.979848482477195, 51.461547796214361 ], [ -79.097906609372131, 51.537938634292303 ], [ -79.2063572036474, 51.53846648402692 ], [ -79.242888002267151, 51.575883558383204 ], [ -79.217217921872802, 51.639273482800732 ], [ -79.305518522320384, 51.681105006461848 ], [ -79.517895318258724, 51.585133393521204 ], [ -79.517798777935582, 49.009119745036998 ], [ -74.67627615106376, 49.000100609213888 ], [ -74.607944695645159, 48.96221775026919 ], [ -74.562290088615711, 49.001562629175723 ], [ -74.504776202898682, 48.92603866901505 ], [ -74.442601733617124, 48.975099662126183 ], [ -74.441019494041527, 48.932035376292021 ], [ -74.332862394189476, 48.92114002557669 ], [ -74.334945817545673, 48.966531324839508 ], [ -74.285232517440377, 48.990586158938406 ], [ -74.30685341191429, 49.041119023626543 ], [ -74.24421363086033, 49.136229628936952 ], [ -74.229446549667273, 49.281585062155926 ], [ -74.294565335895101, 49.289091523941501 ], [ -74.310702377359092, 49.258078990545066 ], [ -74.374842676398231, 49.316433385651678 ], [ -74.432853518167391, 49.323846801967193 ], [ -74.393395345015279, 49.369991140041499 ], [ -74.415701619343935, 49.382013009379605 ], [ -74.374935469729834, 49.442799013060011 ], [ -74.393706640652994, 49.474782579700467 ], [ -74.254526963323201, 49.511968346041272 ], [ -74.252650533795304, 49.567102528044757 ], [ -74.198506248540255, 49.610178317685119 ], [ -74.181497511497156, 49.679182617449257 ], [ -74.141625457503565, 49.679537938847204 ], [ -74.09841815282735, 49.735391377151736 ], [ -74.016619361218318, 49.735327510013313 ], [ -74.016589415838837, 49.801872518293457 ], [ -73.787170982234855, 50.056495629589449 ], [ -73.770337074998793, 50.2153264599404 ], [ -73.684497464680973, 50.21168373727172 ], [ -73.594997076396297, 50.349585265215225 ], [ -73.593429194707852, 50.403465048617775 ], [ -73.57361514593191, 50.359636563220278 ], [ -73.486034764580779, 50.400580425687487 ], [ -73.459726961931906, 50.368210812327739 ], [ -73.250414842093278, 50.692424398094076 ], [ -73.183699247213667, 50.701483754462252 ], [ -73.14707242557698, 50.750542239321447 ], [ -73.023141474548538, 50.697911788954464 ], [ -73.0481791849638, 50.667054114777876 ], [ -73.044625933807069, 50.654459722823731 ], [ -72.94096405364148, 50.713409452546145 ], [ -72.934312324222645, 50.762943434103612 ], [ -72.857900681641425, 50.812444509634709 ], [ -72.752005133560417, 50.820910537914862 ], [ -72.722162721569404, 50.798718593103452 ], [ -72.731350348729706, 50.761784250765459 ], [ -72.703726555891663, 50.781506467216389 ], [ -72.648286737839953, 50.747648244154433 ], [ -72.567190839113778, 50.885429230729166 ], [ -72.573375832499508, 50.750117395743501 ], [ -72.480711567244668, 50.750879134025659 ], [ -72.470280017219338, 50.784273812598528 ], [ -72.397509811883793, 50.813325279625538 ], [ -72.378226629827353, 50.888110075453852 ], [ -72.339739183559004, 50.885669226339608 ], [ -72.269595334337367, 50.951076254882999 ], [ -72.261556930901662, 51.00359071900008 ], [ -72.191667192447809, 51.029393868435434 ], [ -72.197975992391449, 50.99041784347564 ], [ -72.156027500507335, 50.978592714760488 ], [ -72.144714027195946, 51.069418869200177 ], [ -72.074140566569952, 51.150687671662489 ], [ -72.018682532877833, 51.138722835656104 ], [ -72.012993551621364, 51.220198781014162 ], [ -71.977866179604916, 51.211628790892647 ], [ -71.991243461538986, 51.230642722087758 ], [ -71.931866547968809, 51.275410434941186 ], [ -71.870120311300226, 51.277878975881833 ], [ -71.868619034664235, 51.343983305762556 ], [ -71.821280149824545, 51.373384685080055 ], [ -71.756300369151049, 51.339186720477755 ], [ -71.687931897139904, 51.401459025141072 ], [ -71.706026800931696, 51.439817932755702 ], [ -71.580680088210414, 51.508748116118902 ], [ -71.610667446443827, 51.692744197442295 ], [ -71.55163721173831, 51.789169887684984 ], [ -71.574183594959507, 51.814196502991223 ], [ -71.377474447139619, 51.84846821327119 ], [ -71.306620968385985, 51.971610647225681 ], [ -71.206599369705856, 52.039486030044223 ], [ -71.265350359872059, 52.037768716693598 ], [ -71.250263046222429, 52.066007520793178 ], [ -71.140346826311244, 52.166697601679616 ], [ -71.077021649367026, 52.211352460209312 ], [ -70.929017767518275, 52.226899078509028 ], [ -70.881018687776219, 52.278978001270332 ], [ -70.806851262208056, 52.2735753367147 ], [ -70.825558391191791, 52.247701291228815 ], [ -70.792275564174631, 52.233678726260095 ], [ -70.559851046036215, 52.317121283427525 ], [ -70.482013678038712, 52.303945326491949 ], [ -70.313693117509217, 52.343362118561231 ], [ -70.231573154288583, 52.408578628989872 ], [ -70.289822243987615, 52.41845804706292 ], [ -70.172981365334763, 52.542780077009425 ], [ -70.242708985221583, 52.559855241160008 ], [ -70.169669053109814, 52.603941585881003 ], [ -70.228318243810222, 52.634436892167656 ], [ -70.180804913611325, 52.662876248978954 ], [ -70.224548937741815, 52.70330789480667 ], [ -70.123012782802505, 52.751791827643387 ], [ -70.158019118544559, 52.75487579108664 ], [ -70.127352769308857, 52.810498021600203 ], [ -70.029889608212372, 52.83208781270055 ], [ -70.029882586478294, 55.000135978323222 ], [ -67.417243852681537, 55.000135728440334 ], [ -67.440272751904942, 55.012746995281304 ], [ -67.420854740528569, 55.033524444300454 ], [ -67.464635027392902, 55.047469173748901 ], [ -67.41285355767846, 55.093691345717339 ], [ -67.273235599970562, 55.073468889226511 ], [ -67.251817830963574, 55.056079687568889 ], [ -67.286931441176932, 55.028107498995723 ], [ -67.218817469616297, 55.000106934580153 ], [ -66.643240784157186, 55.000102216056803 ], [ -66.750076742861012, 55.086272470981662 ], [ -66.689271963669356, 55.061881664436967 ], [ -66.773105572231074, 55.139439736626557 ], [ -66.739545814303895, 55.144939147131595 ], [ -66.751830523350776, 55.210830020342961 ], [ -66.667964918639413, 55.176744746540741 ], [ -66.82119107852165, 55.305748846416328 ], [ -66.828298882283448, 55.326693591752523 ], [ -66.792881363536225, 55.315637458036576 ], [ -66.806663214340887, 55.356943090125782 ], [ -66.704374763740972, 55.312914164294725 ], [ -66.656928068208131, 55.25246802928212 ], [ -66.615621958468296, 55.276830005610172 ], [ -66.514121441536574, 55.189354698036013 ], [ -66.493675837027496, 55.202688603263738 ], [ -66.259613081023375, 55.00010058936472 ], [ -63.554296985495142, 55.000113215179731 ], [ -63.59840952860857, 55.013558030503731 ], [ -63.566685478580027, 55.03416915260977 ], [ -63.621909206049658, 55.093698324463126 ], [ -63.525773876961445, 55.168032228300902 ], [ -63.553993046792662, 55.188949471262823 ], [ -63.453270847658963, 55.202449198519346 ], [ -63.397259389104192, 55.250089171796354 ], [ -63.614574376674327, 55.228783858616936 ], [ -63.596768105689769, 55.253256341728388 ], [ -63.683691444403401, 55.266868310696459 ], [ -63.519884382559951, 55.310062216313142 ], [ -63.588997192607671, 55.320284977444359 ], [ -63.581608568437048, 55.339396381657274 ], [ -63.322425606003982, 55.364812502352272 ], [ -63.319401137324363, 55.409229383775084 ], [ -63.347265089979821, 55.417007240520327 ], [ -63.673325234264588, 55.423565845885186 ], [ -63.797809807443507, 55.473872170832507 ], [ -63.691270865160199, 55.510844994494754 ], [ -63.653378295907579, 55.555873353732665 ], [ -63.682628872406184, 55.636152733949189 ], [ -63.64209926215495, 55.643347227191519 ], [ -63.766743066922963, 55.654236656160663 ], [ -63.663736976876386, 55.79162723757684 ], [ -63.831993674210068, 55.80448915184104 ], [ -63.822298392263647, 55.838323018596299 ], [ -63.764240724165134, 55.846183920979634 ], [ -63.844606950629071, 55.888213039457092 ], [ -63.796910585609382, 55.929963320587376 ], [ -63.725378372902, 55.930795791061591 ], [ -63.56119718215735, 56.00398949569955 ], [ -63.473331689551664, 55.989710271478287 ], [ -63.491888374361061, 56.006627502784589 ], [ -63.445497188449281, 56.032544150610931 ], [ -63.688456266901539, 56.0255469652041 ], [ -63.669593836321532, 56.039574680114598 ], [ -63.69156974235932, 56.060964740417873 ], [ -63.839482569265193, 56.048687474721589 ], [ -63.826260014907639, 56.082604710844016 ], [ -63.90715091624525, 56.093633094961064 ], [ -63.946880274277852, 56.055827240355669 ], [ -64.035418927676574, 56.061689150327105 ], [ -64.06019701021421, 56.083550819175706 ], [ -64.01777794990555, 56.092939726523831 ], [ -64.002498948165254, 56.131884739148134 ], [ -64.042605616775276, 56.149246669354547 ], [ -63.877616946075051, 56.198412250775839 ], [ -63.860837833441948, 56.227773929749198 ], [ -63.915090314524562, 56.221718587914502 ], [ -63.94156561712569, 56.267275593352075 ], [ -64.122626449867525, 56.261166415185372 ], [ -64.075012214676178, 56.290166673517781 ], [ -64.14996634243748, 56.300833632199662 ], [ -64.090623837048142, 56.332111899171352 ], [ -64.148276388501245, 56.378668053052024 ], [ -64.097188580694407, 56.39372361781092 ], [ -64.18335868304645, 56.429696629350296 ], [ -64.10418315878637, 56.444696385553321 ], [ -63.988990216935598, 56.41083417785255 ], [ -63.869714971256961, 56.44683252722217 ], [ -63.932245814496632, 56.475416904141056 ], [ -63.943547475355146, 56.53983494724671 ], [ -64.058084533775414, 56.614254422885331 ], [ -64.012938702235459, 56.660782503559076 ], [ -64.101739646117139, 56.655700298200088 ], [ -64.143159765566807, 56.703479448982861 ], [ -64.076237476596219, 56.735868341513189 ], [ -64.087572511759319, 56.767480302752077 ], [ -64.008045628267169, 56.778228862733847 ], [ -64.008266448755251, 56.859314134176586 ], [ -63.870565520346005, 56.898453058465336 ], [ -63.910507720875181, 56.926120226043459 ], [ -63.912062268041147, 56.984565818969173 ], [ -63.861643082156192, 56.99748249524923 ], [ -63.902254268935167, 57.077373368847219 ], [ -63.774740588356224, 57.102040033897616 ], [ -63.79607894596495, 57.194292215628472 ], [ -63.764271383368524, 57.221125848471566 ], [ -63.828329125427452, 57.237487505581996 ], [ -63.885360371942696, 57.196681604871976 ], [ -63.842823859636958, 57.346934173079944 ], [ -63.807437590916464, 57.325433600570975 ], [ -63.69803934165715, 57.366989172594316 ], [ -63.728943321342001, 57.409629402064397 ], [ -63.710832561558696, 57.432768632673145 ], [ -63.74143864026221, 57.41915782475774 ], [ -63.768268269964842, 57.526937799009538 ], [ -63.834743115711802, 57.56588332225288 ], [ -63.849059889943753, 57.61796747516307 ], [ -63.737015269042566, 57.662246399014862 ], [ -63.751486895749032, 57.727831054513992 ], [ -63.81064518317514, 57.688913341006717 ], [ -63.883181583929257, 57.715580597572846 ], [ -63.899744779617201, 57.91286304780445 ], [ -63.939465573557854, 57.892029714580467 ], [ -64.049371431308188, 57.931169725504745 ], [ -64.184937407493976, 57.924753620763717 ], [ -64.242145301059637, 58.019116951801969 ], [ -64.219977328486323, 58.034895001050863 ], [ -64.261397556309873, 58.065645811692818 ], [ -64.283764479978174, 58.045506618805447 ], [ -64.431307235972241, 58.068785868673793 ], [ -64.443111607895275, 58.131898359659914 ], [ -64.434443099855898, 58.198816095476978 ], [ -64.219155312990452, 58.246760295880712 ], [ -64.166412858321365, 58.366151353647005 ], [ -64.029727751874418, 58.364566947368616 ], [ -63.985302778649277, 58.443235271586971 ], [ -63.88688463334946, 58.433623005051174 ], [ -63.810239411388096, 58.484206934253031 ], [ -63.853296711346935, 58.50256885909576 ], [ -63.866210039889197, 58.567903825647925 ], [ -64.033658428386659, 58.522848584022015 ], [ -64.102106474900054, 58.562599655405485 ], [ -64.090628232549307, 58.589322513285595 ], [ -64.145834109758297, 58.606239686904843 ], [ -64.043177793558186, 58.701741136327271 ], [ -63.715178318285304, 58.707044399776997 ], [ -63.466472263732648, 58.763655276776134 ], [ -63.580778045899265, 58.852466677485701 ], [ -63.712395811163709, 58.887492810461488 ], [ -64.040879684767049, 58.810854386252309 ], [ -64.077540721903603, 58.76274252393771 ], [ -64.146497567936876, 58.748409198929856 ], [ -64.175612252089394, 58.788049144110431 ], [ -64.23158676953858, 58.786604992611608 ], [ -64.239724168234972, 58.861051120644127 ], [ -64.305253540566994, 58.902024754240017 ], [ -64.46037113205584, 58.908359215777452 ], [ -64.517512941391871, 58.882497727255974 ], [ -64.650978117759024, 58.940638073850735 ], [ -64.732784184781039, 58.950194392472532 ], [ -64.817399543145754, 58.915055239065815 ], [ -64.88945802726937, 58.941639679447896 ], [ -64.834455046074225, 58.958056393541682 ], [ -64.870205808762094, 59.003779707058072 ], [ -64.830953994189002, 59.019557737033075 ], [ -64.82814754448421, 59.072031343256697 ], [ -64.684278514007531, 59.074141399889029 ], [ -64.670668013350095, 59.038807260230683 ], [ -64.492935281029617, 59.01655559450414 ], [ -64.472244242383596, 58.984221695559043 ], [ -64.265387569066476, 59.013998896308593 ], [ -64.368736684956744, 59.098556650891325 ], [ -64.419791609881585, 59.078834442096209 ], [ -64.435380294500561, 59.107140551231737 ], [ -64.489492697776527, 59.103251918061723 ], [ -64.471140862278247, 59.17950384854187 ], [ -64.534940831318977, 59.220533917522815 ], [ -64.489044802105965, 59.270896068170323 ], [ -64.557684276355715, 59.316647769815361 ], [ -64.491655189468574, 59.349647769656762 ], [ -64.534187504732955, 59.366036960219454 ], [ -64.525080543193951, 59.399842508364308 ], [ -64.462468438220327, 59.411369962568862 ], [ -64.514303586267715, 59.422926396321657 ], [ -64.324060108677131, 59.511345555918048 ], [ -64.442017224428596, 59.534984165677159 ], [ -64.675413665505786, 59.445845316441179 ], [ -64.810778326506252, 59.497986915941972 ], [ -64.839889198119835, 59.556655029236452 ], [ -64.804136705416056, 59.590072484865061 ], [ -64.809857798645552, 59.654462788362331 ], [ -64.748635915379936, 59.713296124760937 ], [ -64.793806310315958, 59.736157495514369 ], [ -64.812418707307543, 59.78243607543709 ], [ -64.789833617478749, 59.807436403841123 ], [ -64.819612061428103, 59.833409463582328 ], [ -64.725190609432673, 59.861298279472784 ], [ -64.762496581427399, 59.890299121312871 ], [ -64.68013247996852, 59.880215253281143 ], [ -64.620901689856012, 59.929379728072135 ], [ -64.775128616622638, 59.982219442411072 ], [ -64.837970015365059, 59.964829171482194 ], [ -64.920622757753549, 60.065501904889423 ], [ -64.722064515680614, 60.057361100233841 ], [ -64.709313174083846, 60.083500523523092 ], [ -64.759339141266864, 60.101168330167596 ], [ -64.768448713579389, 60.109891093670299 ], [ -64.634726304976297, 60.104999438288552 ], [ -64.58041465243565, 60.131499189515807 ], [ -64.610696668225756, 60.170916392645559 ], [ -64.755440542429682, 60.171420849531842 ], [ -64.852303262299856, 60.226423576711824 ], [ -64.859049784024378, 60.264173020547773 ], [ -64.531254834903237, 60.308951593524917 ], [ -64.852371304156051, 60.369654778634633 ], [ -64.828987934823886, 60.340254211239731 ], [ -64.862400218533622, 60.293629113820984 ], [ -64.947822628004332, 60.269229385967662 ], [ -64.965342325312818, 60.17851991262215 ], [ -65.141354097197407, 60.063803601147185 ], [ -65.138679800218867, 60.028840380571012 ], [ -65.235615201916389, 59.925939317941911 ], [ -65.557808406295948, 59.741559378718001 ], [ -65.565521520588817, 59.690496898999086 ], [ -65.43735869120664, 59.572124357023078 ], [ -65.529927773860578, 59.467525528874887 ], [ -65.85291533044979, 59.282466019684662 ], [ -65.865784690189798, 59.242375904942506 ], [ -65.799850186058947, 59.207127083766991 ], [ -65.924997871298558, 59.057375530519195 ], [ -65.923178237483569, 58.993051702866317 ], [ -65.942916691650936, 58.968431435167872 ], [ -66.056638157598513, 58.967423817824859 ], [ -65.977610838143107, 58.935499809356166 ], [ -65.952531000164328, 58.958015018411253 ], [ -65.990105125346545, 58.916367879143507 ], [ -66.09508780572115, 58.861314154746523 ], [ -66.473127013856754, 58.866283009573401 ], [ -66.520814767002989, 58.803234398619168 ], [ -66.496344789436691, 58.765297981747572 ], [ -66.579664629059778, 58.664475589764535 ], [ -66.686329112760589, 58.641965014653266 ], [ -66.665889736307122, 58.601789709939048 ], [ -66.73685202266779, 58.515918840202602 ], [ -67.014603775997955, 58.506161670343296 ], [ -67.051034211649821, 58.452692189047411 ], [ -67.38819111003059, 58.312454617894367 ], [ -67.440234850930779, 58.33249365122203 ], [ -67.397596558591843, 58.362105921529952 ], [ -67.449826040415374, 58.355672279895018 ], [ -67.430340285892598, 58.389762780797234 ], [ -67.503271117089284, 58.462760300757424 ], [ -67.551033404579712, 58.388984852048608 ], [ -67.552010602326433, 58.437030107730919 ], [ -67.620322950229706, 58.424736964052691 ], [ -67.728640421838648, 58.489613617131091 ], [ -67.918744097282371, 58.531358437346242 ], [ -68.000000005058979, 58.590450016522681 ], [ -68.243232477643474, 58.583222309198597 ], [ -68.278082869166099, 58.678207412656178 ], [ -68.259372763895826, 58.818991303668689 ], [ -68.313879901952788, 58.857754599934445 ], [ -68.548069122243547, 58.940790202599267 ], [ -68.892014787617271, 58.923914440358082 ], [ -69.210208529302449, 59.228672979592432 ], [ -69.232419134375419, 59.346266120974825 ], [ -69.390653927124191, 59.363435400776957 ], [ -69.62880731811677, 59.473388884301229 ], [ -69.637416462698909, 59.528762794626488 ], [ -69.483384000056162, 59.631154492438853 ], [ -69.540086827672184, 59.744042108284788 ], [ -69.47005150002532, 59.799752082919539 ], [ -69.528705966381054, 59.816768698941033 ], [ -69.489970798122783, 59.872452879001756 ], [ -69.625887531668226, 59.918516196003928 ], [ -69.542683802692963, 60.093940018045707 ], [ -69.590116412069875, 60.117904097922647 ], [ -69.559804941302033, 60.154046301951745 ], [ -69.591873512165208, 60.169280202620541 ], [ -69.58627253826063, 60.228005213054836 ], [ -69.638846946469386, 60.243121620090811 ], [ -69.661743572973194, 60.356328912130152 ], [ -69.73223130950079, 60.436647810924519 ], [ -69.620908094455643, 60.583004695224659 ], [ -69.674667491617953, 60.673179100756563 ], [ -69.356654001270172, 60.814335402295328 ], [ -69.389191424507658, 60.868178217482424 ], [ -69.37006683624341, 60.915933891448979 ], [ -69.446021626305281, 60.957916690271595 ], [ -69.50345003665538, 61.068010610487775 ], [ -69.613299979769451, 61.078597721555454 ], [ -69.697200771057013, 60.995562897723353 ], [ -69.68217658383, 60.942955894523394 ], [ -69.762177440952939, 60.918261919909078 ], [ -69.964242468699055, 60.945330339891662 ], [ -69.911172570932166, 61.001640012657347 ], [ -69.949291005319694, 61.040925718230483 ], [ -70.023663798469968, 60.997780206244897 ], [ -69.991527024973323, 60.948960765167385 ], [ -70.086251473375086, 60.961518993127058 ], [ -70.154053506663089, 61.011204306925229 ], [ -70.105804735836287, 61.05362301981112 ], [ -70.130048745190706, 61.090792586972107 ], [ -70.30275128010615, 61.101182621806522 ], [ -70.668349163295801, 61.055620916186058 ], [ -70.738017798011555, 61.085088719670274 ], [ -70.880870963357651, 61.07898227926858 ], [ -70.989461358313491, 61.128161883188824 ], [ -71.444756322748191, 61.181197918652693 ], [ -71.432382077588997, 61.223127778638407 ], [ -71.607041927660049, 61.263210976999289 ], [ -71.632184431028691, 61.301837070859236 ], [ -71.59522633913069, 61.313024585562694 ], [ -71.636613898742638, 61.325471721075189 ], [ -71.563582639914102, 61.4076447976704 ], [ -71.565243707350405, 61.545288584725526 ], [ -71.514197767570664, 61.610678120965609 ], [ -72.029212923287147, 61.729983219115709 ], [ -72.209748657898245, 61.874707511113478 ], [ -72.438817879806493, 61.906168593583608 ], [ -72.577495345075249, 61.976981899443473 ], [ -72.613662270452963, 62.112201092358085 ], [ -73.045625732543698, 62.20141910600281 ], [ -73.162197821463437, 62.262700184542076 ], [ -73.191752877198581, 62.315020087978283 ], [ -73.586656081650702, 62.413973515269547 ], [ -73.671813100575079, 62.480220521712042 ], [ -73.845546819457894, 62.464947289054166 ], [ -74.019374495141733, 62.367630810939609 ], [ -74.257418093707741, 62.325204985833508 ], [ -74.404055260828059, 62.250242484086911 ], [ -74.865073387914933, 62.240612000801029 ], [ -75.342002831724272, 62.316912909690011 ], [ -75.692519972192272, 62.294987805785851 ], [ -76.433047579125514, 62.424650580841998 ], [ -76.79696092473813, 62.522412913834884 ], [ -77.499999959000888, 62.57944802246044 ] ] ] } },
16 +{ "type": "Feature", "properties": { "code": "11", "name": "Gaspésie–Îles-de-la-Madeleine", "geography_id": "region-11" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -63.526312062315604, 48.929185506696911 ], [ -64.197555637041319, 49.337163253077485 ], [ -64.850078044324462, 49.59350132429784 ], [ -65.531769891686594, 49.712144271340208 ], [ -65.880200096382239, 49.714062547769934 ], [ -66.157544782765555, 49.678646848022836 ], [ -66.534672089584603, 49.553453092337158 ], [ -67.153339142634252, 49.18906403276106 ], [ -66.832032993541887, 48.985077062530785 ], [ -66.732989366008425, 49.017530356093509 ], [ -66.699318005571314, 48.988611913225242 ], [ -66.70441521193213, 48.902873345905128 ], [ -66.537315142547982, 48.957826533637991 ], [ -65.948498198459305, 48.567089144875254 ], [ -66.97415999578601, 48.30315642898266 ], [ -66.836925357775854, 48.147741707786416 ], [ -66.900450576427687, 48.200447649982998 ], [ -66.934625790649989, 48.181190786992964 ], [ -66.879636407319737, 48.136954025567931 ], [ -67.098450340673082, 48.077948220056257 ], [ -67.277547792467402, 48.031538031877908 ], [ -67.374603194612703, 48.194892593960361 ], [ -67.666958996536451, 48.116718144968559 ], [ -67.58487404702953, 48.045218176325555 ], [ -67.607536966097143, 47.936339627345298 ], [ -67.374415565792887, 47.84538949763504 ], [ -67.336769418140818, 47.872253538527545 ], [ -67.347739616371072, 47.895957969872214 ], [ -67.208108663054404, 47.875968189503354 ], [ -67.059661152539903, 47.935390641293971 ], [ -66.955637499910324, 47.892402808159737 ], [ -66.964887067403254, 47.941317444431547 ], [ -66.920116708432843, 47.984560594233585 ], [ -66.698683529721094, 48.005365334914629 ], [ -66.401026111899441, 48.089610021269245 ], [ -66.305883564866477, 48.033137791016237 ], [ -65.858134126957879, 48.022245784620416 ], [ -65.606456951025365, 47.933633887353203 ], [ -65.53617443098625, 47.828632179494605 ], [ -65.111713642296422, 47.921131338005104 ], [ -64.421962203141419, 48.22057417553853 ], [ -63.791660832019666, 48.220571676596435 ], [ -62.986360783867354, 47.139731658661105 ], [ -62.299949504597343, 46.840006640487282 ], [ -61.399363599355738, 46.840003348958327 ], [ -61.350472231001646, 47.009725571627641 ], [ -60.991847767751537, 47.329446709088174 ], [ -60.762673116018689, 47.423334720829466 ], [ -60.403770302987489, 47.761111394002157 ], [ -60.468597199247426, 48.666667945248911 ], [ -62.999128701777302, 48.666683203520115 ], [ -62.999128389281516, 48.701792945343961 ], [ -63.526312062315604, 48.929185506696911 ] ] ] } },
17 +{ "type": "Feature", "properties": { "code": "12", "name": "Chaudière-Appalaches", "geography_id": "region-12" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -70.003259647392454, 47.278472754391821 ], [ -70.197392286185178, 47.4119438131845 ], [ -70.454942106735729, 47.253504708328975 ], [ -70.524363351699876, 47.164836879234322 ], [ -70.674537858140056, 47.065671184499088 ], [ -70.711234913115632, 46.997171079967316 ], [ -70.990663810047025, 46.850755526326687 ], [ -71.170227220482928, 46.844262785262707 ], [ -71.271981300560526, 46.750062305450129 ], [ -71.675526141722514, 46.652286340644793 ], [ -71.872033345652156, 46.677398213824553 ], [ -72.054319572163649, 46.570510300459453 ], [ -71.760157956952881, 46.363787375326432 ], [ -71.617549172098904, 46.451586857829597 ], [ -71.566224339937676, 46.413681577817322 ], [ -71.531491870016453, 46.434337802825333 ], [ -71.478669918390267, 46.395682097777247 ], [ -71.508472841928167, 46.338379081630066 ], [ -71.37178660417247, 46.268099264601602 ], [ -71.419148667684681, 46.22327321044817 ], [ -71.485503185343319, 46.213718443289757 ], [ -71.533648584242087, 46.149046630270561 ], [ -71.509753788725035, 46.137084866362166 ], [ -71.68592485973376, 45.967042773367879 ], [ -71.455612821759743, 45.872254721526737 ], [ -71.467201609349175, 45.822228836986341 ], [ -71.392621357926842, 45.768845798672857 ], [ -71.340525510401875, 45.809642148014156 ], [ -71.348405226966108, 45.872239132696443 ], [ -71.178095409762548, 45.815105797795113 ], [ -71.135149628077386, 45.860284381604686 ], [ -71.162434229597238, 45.92344135188695 ], [ -71.129581244174034, 45.95733048468945 ], [ -71.070682307764542, 45.928350036604272 ], [ -71.094050537632825, 45.907066168491745 ], [ -71.029533777173654, 45.859131114369049 ], [ -70.898330992020945, 45.841799941594758 ], [ -70.895570053244185, 45.780116844051172 ], [ -70.651847606508028, 45.802721941836523 ], [ -70.631861631617895, 45.778682976592556 ], [ -70.499781722932013, 45.82643031702824 ], [ -70.47131985523535, 45.788828916834198 ], [ -70.398910285783771, 45.796630843837043 ], [ -70.258821969175713, 45.891168888382751 ], [ -70.239141689209902, 45.943943592368328 ], [ -70.315858918346763, 45.962990534433899 ], [ -70.283941915516905, 45.995487485734856 ], [ -70.317995564887028, 46.019075001648375 ], [ -70.278321087426392, 46.056285837618034 ], [ -70.301909178577887, 46.082360012164209 ], [ -70.25311389392175, 46.100119451892809 ], [ -70.23628333641328, 46.145233328694623 ], [ -70.292691653018395, 46.191463893040343 ], [ -70.19118891123486, 46.350216688730534 ], [ -70.056500010527458, 46.415577781853081 ], [ -69.997111079176094, 46.695520000988481 ], [ -69.625290475616112, 47.06672887090172 ], [ -69.972934067653043, 47.291575628482612 ], [ -70.003259647392454, 47.278472754391821 ] ] ] } },
18 +{ "type": "Feature", "properties": { "code": "13", "name": "Laval", "geography_id": "region-13" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -73.523098406737788, 45.697883228829149 ], [ -73.727842978815744, 45.680403275159648 ], [ -73.896138936078145, 45.525546375861929 ], [ -73.766120060744754, 45.511434496036763 ], [ -73.682520913526744, 45.551478420269603 ], [ -73.630812002772728, 45.628529440991592 ], [ -73.523098406737788, 45.697883228829149 ] ] ] } },
19 +{ "type": "Feature", "properties": { "code": "14", "name": "Lanaudière", "geography_id": "region-14" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -73.957427314645344, 47.38817074166036 ], [ -74.511510586554749, 47.762913212232824 ], [ -74.889415295400852, 47.762621155404709 ], [ -74.539436575248047, 47.509856218381429 ], [ -74.426029283960986, 47.505367987142002 ], [ -74.513718413789135, 47.40627889002478 ], [ -74.542098954218844, 47.313405520703881 ], [ -74.586963018121963, 47.316497063069654 ], [ -74.618226382021305, 47.250896100351369 ], [ -74.656983658112495, 47.243953364959296 ], [ -74.489744837652438, 47.22455852689361 ], [ -74.434525905155127, 47.15752646333474 ], [ -74.466053164340607, 47.13569450095715 ], [ -74.415173309693486, 47.100718149717366 ], [ -74.574715715395186, 46.99750433532072 ], [ -74.427392436431504, 46.896586257755864 ], [ -74.484721321271749, 46.86699556985252 ], [ -74.500340794897255, 46.59477283508123 ], [ -74.444621339706259, 46.620774669277104 ], [ -74.343615378356162, 46.5493055440391 ], [ -74.477483679579805, 46.454093164338069 ], [ -74.334041294899052, 46.353923736550549 ], [ -74.413076932364106, 46.298894931744066 ], [ -74.26721623938586, 46.198125725858787 ], [ -74.182603892668169, 46.250056494338324 ], [ -74.032625580078559, 46.148316841104759 ], [ -74.077440800040819, 46.117145459953605 ], [ -73.960491739402528, 46.064641436779397 ], [ -74.005834560587061, 46.032789418472262 ], [ -73.97918840416655, 46.01460024695178 ], [ -74.00233392447052, 45.996972199850859 ], [ -73.730149196497351, 45.754492376787226 ], [ -73.76476682775801, 45.713194191018822 ], [ -73.84477176960246, 45.720967302088077 ], [ -73.783452191656394, 45.680274380125994 ], [ -73.460920689981492, 45.703787760813128 ], [ -73.385558307705367, 45.789242782996006 ], [ -73.279070026095695, 45.842176303919025 ], [ -73.156100007605318, 46.046304470734775 ], [ -72.96991409108567, 46.111707838988337 ], [ -72.936245111859108, 46.151235214525087 ], [ -73.036707876104856, 46.156588056056776 ], [ -73.216691142333531, 46.279005524282347 ], [ -73.19567873073143, 46.294176916279092 ], [ -73.228451217800369, 46.316676010740366 ], [ -73.176173393606106, 46.323285879783676 ], [ -73.203427767519813, 46.358386264381274 ], [ -73.167002977363722, 46.382299797543638 ], [ -73.332946385296282, 46.498873757718741 ], [ -73.283001031209309, 46.533309347233555 ], [ -73.33650850439642, 46.601265189479548 ], [ -73.446013253223484, 46.59626519332074 ], [ -73.466958351044283, 46.709488617030409 ], [ -73.421237662478291, 46.890298277735525 ], [ -73.495076997383123, 46.912357008464134 ], [ -73.525943266818246, 46.862496996865673 ], [ -73.563991198189242, 46.883434344803256 ], [ -73.665448405545305, 46.863900769558484 ], [ -73.645947301680309, 46.937462374988222 ], [ -73.754786410209519, 47.066608391718958 ], [ -73.853251821247341, 47.151078007734448 ], [ -73.910582251261673, 47.099082725786459 ], [ -74.028582863686779, 47.177838740316375 ], [ -73.99897777547919, 47.345654068294458 ], [ -73.957427314645344, 47.38817074166036 ] ] ] } },
20 +{ "type": "Feature", "properties": { "code": "15", "name": "Laurentides", "geography_id": "region-15" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -74.889415295400852, 47.762621155404709 ], [ -75.538612226716268, 47.763261811734921 ], [ -75.524277140342051, 47.710519458573089 ], [ -75.421754140900944, 47.634641067188355 ], [ -75.41419349614057, 47.588627982766532 ], [ -75.36959914065028, 47.627142395229775 ], [ -75.360287105443618, 47.570213293959497 ], [ -75.308026222508715, 47.547467837843364 ], [ -75.46043712927208, 47.367888553088342 ], [ -75.473452526701323, 47.282386261067401 ], [ -75.594826730255107, 47.185435739165811 ], [ -75.625835534409759, 47.180936354364015 ], [ -75.647836725372869, 47.281748369106872 ], [ -75.747833523399976, 47.272918806008541 ], [ -75.798865316091579, 47.318877351697282 ], [ -75.976184969153806, 47.203159978619652 ], [ -76.10059718994944, 47.183105858919767 ], [ -76.102355610337128, 47.03047835950585 ], [ -76.148472638218152, 47.006168967013544 ], [ -76.128166037249656, 46.921028389246096 ], [ -75.971081490477729, 46.87839128836498 ], [ -75.890005118585236, 46.815501543624606 ], [ -75.746957404129944, 46.844824405657036 ], [ -75.745213974613961, 46.438256989474063 ], [ -75.706004087524875, 46.438816366965149 ], [ -75.707042202236863, 46.309165822329106 ], [ -75.814570296980875, 46.306351082622271 ], [ -75.783033258740502, 46.282010335183763 ], [ -75.813144849910231, 46.224916684772147 ], [ -75.79901348555326, 46.174121622782152 ], [ -75.768311307229538, 46.17417628912839 ], [ -75.764419246762529, 46.022219814433356 ], [ -75.697274975935656, 46.023889317706846 ], [ -75.784749726565195, 45.959745395087303 ], [ -75.620766718698903, 45.99471255220395 ], [ -75.599159410171552, 45.971449813692011 ], [ -75.40720978574069, 45.972950674008828 ], [ -75.407157497500535, 46.097862133633278 ], [ -75.176360583907524, 46.103409070907723 ], [ -75.117388337025787, 46.215204782338553 ], [ -75.074034858220458, 46.215292787572253 ], [ -75.089790696686322, 46.103131465914828 ], [ -74.939544277084366, 46.105533128782604 ], [ -74.939450909792754, 46.061728800812439 ], [ -74.858617777396333, 46.060743378305773 ], [ -74.801302826693032, 45.94509613192642 ], [ -74.698759978142306, 45.971776245459942 ], [ -74.807766485218835, 45.638317997372383 ], [ -74.643899374578083, 45.637529184563121 ], [ -74.476482999001448, 45.595648313571793 ], [ -74.37291579469715, 45.561252200253875 ], [ -74.329111097896003, 45.507640094275573 ], [ -74.09446811367421, 45.45304376567298 ], [ -73.977906935939032, 45.464431597479667 ], [ -73.745397510363631, 45.668696367568742 ], [ -73.84477176960246, 45.720967302088077 ], [ -73.76476682775801, 45.713194191018822 ], [ -73.730149196497351, 45.754492376787226 ], [ -74.00233392447052, 45.996972199850859 ], [ -73.97918840416655, 46.01460024695178 ], [ -74.005834560587061, 46.032789418472262 ], [ -73.960491739402528, 46.064641436779397 ], [ -74.077440800040819, 46.117145459953605 ], [ -74.032625580078559, 46.148316841104759 ], [ -74.182603892668169, 46.250056494338324 ], [ -74.26721623938586, 46.198125725858787 ], [ -74.413076932364106, 46.298894931744066 ], [ -74.334041294899052, 46.353923736550549 ], [ -74.477483679579805, 46.454093164338069 ], [ -74.343615378356162, 46.5493055440391 ], [ -74.444621339706259, 46.620774669277104 ], [ -74.500340794897255, 46.59477283508123 ], [ -74.484721321271749, 46.86699556985252 ], [ -74.427392436431504, 46.896586257755864 ], [ -74.574715715395186, 46.99750433532072 ], [ -74.415173309693486, 47.100718149717366 ], [ -74.466053164340607, 47.13569450095715 ], [ -74.434525905155127, 47.15752646333474 ], [ -74.489744837652438, 47.22455852689361 ], [ -74.656983658112495, 47.243953364959296 ], [ -74.618226382021305, 47.250896100351369 ], [ -74.586963018121963, 47.316497063069654 ], [ -74.542098954218844, 47.313405520703881 ], [ -74.513718413789135, 47.40627889002478 ], [ -74.426279942882019, 47.506031620532141 ], [ -74.539436575248047, 47.509856218381429 ], [ -74.889415295400852, 47.762621155404709 ] ] ] } },
21 +{ "type": "Feature", "properties": { "code": "16", "name": "Montérégie", "geography_id": "region-16" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -72.799346083858396, 46.014485940663505 ], [ -73.04069907112445, 46.107444908834275 ], [ -73.156100007605318, 46.046304470734775 ], [ -73.279070026095695, 45.842176303919025 ], [ -73.47744088680966, 45.698959603620651 ], [ -73.5314455159169, 45.536469174161319 ], [ -73.522251909506068, 45.450625746631282 ], [ -73.5513636189621, 45.421681201066626 ], [ -73.76795484614351, 45.418680681935363 ], [ -73.822179339343961, 45.385403014947222 ], [ -73.949035968336304, 45.401069410895943 ], [ -73.99660217387256, 45.457487266119614 ], [ -74.325109720389278, 45.506414695577277 ], [ -74.381036028608563, 45.565581947107226 ], [ -74.472032409136702, 45.302858428698052 ], [ -74.319521600300121, 45.18850557629213 ], [ -74.442324465755405, 45.131213207529605 ], [ -74.495215240429971, 45.064741938944756 ], [ -74.669796891160487, 45.005566449379174 ], [ -73.139875247712169, 45.014416334322121 ], [ -73.091315679638683, 45.196142179396773 ], [ -73.058243389196775, 45.214581402871616 ], [ -73.063446871099544, 45.267768927457155 ], [ -73.046169602522852, 45.293109989836111 ], [ -73.015781549062282, 45.272101883006535 ], [ -73.04885686885568, 45.325351957532646 ], [ -72.989998687698574, 45.341932185325028 ], [ -72.883053652889188, 45.265547434664327 ], [ -72.799820629858019, 45.537984283382279 ], [ -72.604923587793806, 45.53768564860286 ], [ -72.586004737387029, 45.498289520969578 ], [ -72.448891019697939, 45.493590727210915 ], [ -72.454395190654509, 45.451382224352827 ], [ -72.404264784013307, 45.449202259034848 ], [ -72.402882465436264, 45.528484335743826 ], [ -72.359675278394604, 45.589394511454259 ], [ -72.315550853686489, 45.599224255084614 ], [ -72.601867426215662, 45.792602698740659 ], [ -72.646156870849509, 45.755786919452696 ], [ -72.704155456974121, 45.758843585977012 ], [ -72.853036717613676, 45.864013639875502 ], [ -72.752553475784978, 45.943010762987754 ], [ -72.804673659567371, 45.980952815260665 ], [ -72.799346083858396, 46.014485940663505 ] ] ] } },
22 +{ "type": "Feature", "properties": { "code": "17", "name": "Centre-du-Québec", "geography_id": "region-17" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -72.06684778784583, 46.569760279646282 ], [ -72.190325076722743, 46.538065806913323 ], [ -72.26394048060321, 46.435427113594983 ], [ -72.457086254740091, 46.38262139491821 ], [ -72.578425282272335, 46.29506686065406 ], [ -72.992587729660826, 46.1119021306462 ], [ -72.799346083858396, 46.014485940663505 ], [ -72.804673659567371, 45.980952815260665 ], [ -72.752553475784978, 45.943010762987754 ], [ -72.853036717613676, 45.864013639875502 ], [ -72.704155456974121, 45.758843585977012 ], [ -72.646156870849509, 45.755786919452696 ], [ -72.601867426215662, 45.792602698740659 ], [ -72.578024695303029, 45.752536001587849 ], [ -72.297674910233312, 45.589406559848264 ], [ -72.237239174723115, 45.646239550115112 ], [ -72.311259830468998, 45.688733854358091 ], [ -72.22710020920654, 45.756215283087457 ], [ -72.206338858940342, 45.696387398495901 ], [ -72.029723388488179, 45.853250091340122 ], [ -71.921234294343748, 45.796312679190102 ], [ -71.861549478822056, 45.798949709074833 ], [ -71.770166310577153, 45.885008003950425 ], [ -71.541554867594229, 45.787721896821793 ], [ -71.46079531428677, 45.819112838404514 ], [ -71.455612821759743, 45.872254721526737 ], [ -71.68592485973376, 45.967042773367879 ], [ -71.509753788725035, 46.137084866362166 ], [ -71.533648584242087, 46.149046630270561 ], [ -71.485503185343319, 46.213718443289757 ], [ -71.419148667684681, 46.22327321044817 ], [ -71.37178660417247, 46.268099264601602 ], [ -71.508472841928167, 46.338379081630066 ], [ -71.478669918390267, 46.395682097777247 ], [ -71.531491870016453, 46.434337802825333 ], [ -71.566224339937676, 46.413681577817322 ], [ -71.617549172098904, 46.451586857829597 ], [ -71.760157956952881, 46.363787375326432 ], [ -72.06684778784583, 46.569760279646282 ] ] ] } }
23 +]
24 +}
added web/styles/globals.css +591 −0
@@ -0,0 +1,591 @@
1 +/**
2 + * =============================================================================
3 + * QWHPI — Quebec Weekly Housing Price Index
4 + * Author : Simon-Pierre Boucher
5 + * Contact : contact@spboucher.ai
6 + * File : web/styles/globals.css
7 + * Purpose : Global styles — validated palette tokens (light + dark),
8 + * editorial layout, refined cards, hero, chart chrome.
9 + * =============================================================================
10 + */
11 +
12 +:root {
13 + color-scheme: light;
14 + --surface-0: #f6f5f1;
15 + --surface-1: #fcfcfb;
16 + --surface-2: #f0efec;
17 + --border: #e3e2db;
18 + --border-strong: #d2d1c8;
19 + --text-primary: #0b0b0b;
20 + --text-secondary: #52514e;
21 + --text-muted: #716f66;
22 + --series-1: #2a78d6;
23 + --series-2: #eb6834;
24 + --series-3: #1baf7a;
25 + --series-4: #eda100;
26 + --series-5: #e87ba4;
27 + --band: rgba(42, 120, 214, 0.14);
28 + --accent-soft: rgba(42, 120, 214, 0.08);
29 + --good: #008300;
30 + --serious: #e34948;
31 + --shadow-card: 0 1px 2px rgba(20, 20, 15, 0.04), 0 4px 16px rgba(20, 20, 15, 0.05);
32 + --shadow-card-hover: 0 2px 4px rgba(20, 20, 15, 0.06), 0 10px 28px rgba(20, 20, 15, 0.09);
33 +}
34 +
35 +:root[data-theme="dark"] {
36 + color-scheme: dark;
37 + --surface-0: #131312;
38 + --surface-1: #1a1a19;
39 + --surface-2: #262624;
40 + --border: #33332f;
41 + --border-strong: #45453f;
42 + --text-primary: #ffffff;
43 + --text-secondary: #c3c2b7;
44 + --text-muted: #98968d;
45 + --series-1: #3987e5;
46 + --series-2: #d95926;
47 + --series-3: #199e70;
48 + --series-4: #c98500;
49 + --series-5: #d55181;
50 + --band: rgba(57, 135, 229, 0.22);
51 + --accent-soft: rgba(57, 135, 229, 0.12);
52 + --good: #37a437;
53 + --serious: #e66767;
54 + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.25);
55 + --shadow-card-hover: 0 2px 4px rgba(0, 0, 0, 0.35), 0 10px 28px rgba(0, 0, 0, 0.35);
56 +}
57 +
58 +@media (prefers-color-scheme: dark) {
59 + :root:not([data-theme="light"]) {
60 + color-scheme: dark;
61 + --surface-0: #131312;
62 + --surface-1: #1a1a19;
63 + --surface-2: #262624;
64 + --border: #33332f;
65 + --border-strong: #45453f;
66 + --text-primary: #ffffff;
67 + --text-secondary: #c3c2b7;
68 + --text-muted: #98968d;
69 + --series-1: #3987e5;
70 + --series-2: #d95926;
71 + --series-3: #199e70;
72 + --series-4: #c98500;
73 + --series-5: #d55181;
74 + --band: rgba(57, 135, 229, 0.22);
75 + --accent-soft: rgba(57, 135, 229, 0.12);
76 + --good: #37a437;
77 + --serious: #e66767;
78 + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.25);
79 + --shadow-card-hover: 0 2px 4px rgba(0, 0, 0, 0.35), 0 10px 28px rgba(0, 0, 0, 0.35);
80 + }
81 +}
82 +
83 +* { box-sizing: border-box; }
84 +
85 +html { scroll-behavior: smooth; }
86 +
87 +body {
88 + margin: 0;
89 + background:
90 + radial-gradient(1200px 400px at 50% -200px, var(--accent-soft), transparent),
91 + var(--surface-0);
92 + color: var(--text-primary);
93 + font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI",
94 + Roboto, "Helvetica Neue", sans-serif);
95 + line-height: 1.55;
96 + font-feature-settings: "tnum";
97 +}
98 +
99 +a { color: var(--series-1); text-decoration: none; }
100 +a:hover { text-decoration: underline; }
101 +
102 +.container { max-width: 1160px; margin: 0 auto; padding: 0 24px 24px; }
103 +
104 +/* ------------------------------------------------------------------ header */
105 +.site-header {
106 + position: sticky; top: 0; z-index: 40;
107 + display: flex; align-items: center; justify-content: space-between;
108 + gap: 16px; flex-wrap: wrap;
109 + padding: 12px 24px;
110 + background: color-mix(in srgb, var(--surface-1) 82%, transparent);
111 + backdrop-filter: blur(14px);
112 + -webkit-backdrop-filter: blur(14px);
113 + border-bottom: 1px solid var(--border);
114 +}
115 +.brand {
116 + display: flex; align-items: baseline; gap: 10px;
117 + font-family: var(--font-display, serif);
118 + font-weight: 700; font-size: 20px; letter-spacing: -0.01em;
119 + color: var(--text-primary);
120 +}
121 +.brand:hover { text-decoration: none; }
122 +.brand small {
123 + font-family: var(--font-sans);
124 + color: var(--text-muted); font-weight: 400; font-size: 12.5px;
125 +}
126 +.site-header nav { display: flex; gap: 4px; flex-wrap: wrap; }
127 +.site-header nav a {
128 + color: var(--text-secondary); font-size: 14px; font-weight: 500;
129 + padding: 6px 12px; border-radius: 999px;
130 + transition: background 0.15s, color 0.15s;
131 +}
132 +.site-header nav a:hover {
133 + color: var(--text-primary); background: var(--surface-2);
134 + text-decoration: none;
135 +}
136 +
137 +.site-footer {
138 + border-top: 1px solid var(--border);
139 + color: var(--text-muted); font-size: 13px;
140 + padding: 28px 24px 36px; text-align: center; margin-top: 56px;
141 +}
142 +
143 +/* -------------------------------------------------------------------- hero */
144 +.hero {
145 + display: flex; align-items: flex-end; justify-content: space-between;
146 + gap: 24px; flex-wrap: wrap;
147 + padding: 40px 0 8px;
148 +}
149 +.hero h1 {
150 + font-family: var(--font-display, serif);
151 + font-size: clamp(28px, 4vw, 42px);
152 + font-weight: 700; letter-spacing: -0.02em;
153 + margin: 0 0 6px; line-height: 1.15;
154 +}
155 +.hero .lede { margin: 0; }
156 +.hero-kicker {
157 + display: inline-flex; align-items: center; gap: 8px;
158 + font-size: 12px; font-weight: 600; letter-spacing: 0.08em;
159 + text-transform: uppercase; color: var(--series-1);
160 + margin-bottom: 12px;
161 +}
162 +.hero-kicker::before {
163 + content: ""; width: 22px; height: 2px; background: var(--series-1);
164 + border-radius: 2px;
165 +}
166 +
167 +/* ------------------------------------------------------------------- cards */
168 +.card {
169 + background: var(--surface-1);
170 + border: 1px solid var(--border);
171 + border-radius: 16px;
172 + padding: 20px;
173 + box-shadow: var(--shadow-card);
174 + transition: box-shadow 0.2s, transform 0.2s;
175 +}
176 +.card.hoverable:hover {
177 + box-shadow: var(--shadow-card-hover);
178 + transform: translateY(-1px);
179 +}
180 +.card-title {
181 + font-size: 13px; font-weight: 600; letter-spacing: 0.02em;
182 + color: var(--text-secondary); margin: 0 0 10px;
183 +}
184 +
185 +.stat-tile { display: flex; flex-direction: column; gap: 3px; }
186 +.stat-tile .label {
187 + font-size: 12px; font-weight: 600; letter-spacing: 0.05em;
188 + text-transform: uppercase; color: var(--text-muted);
189 +}
190 +.stat-tile .value {
191 + font-size: 32px; font-weight: 700; letter-spacing: -0.02em;
192 + font-family: var(--font-display, serif); line-height: 1.15;
193 +}
194 +.stat-tile .value.money { font-variant-numeric: tabular-nums; }
195 +.stat-tile .delta { font-size: 13px; color: var(--text-secondary); }
196 +.delta.up { color: var(--good); font-weight: 600; }
197 +.delta.down { color: var(--serious); font-weight: 600; }
198 +
199 +.grid { display: grid; gap: 16px; }
200 +.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr)); }
201 +.grid.cols-5 { grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr)); }
202 +.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); }
203 +
204 +/* ---------------------------------------------------------------- controls */
205 +.controls {
206 + display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
207 + margin: 18px 0;
208 +}
209 +.controls label {
210 + font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
211 + text-transform: uppercase; color: var(--text-muted);
212 +}
213 +select, button.ctrl, input[type="date"] {
214 + background: var(--surface-1); color: var(--text-primary);
215 + border: 1px solid var(--border-strong); border-radius: 10px;
216 + padding: 8px 13px; font-size: 14px; font-family: inherit;
217 + box-shadow: 0 1px 2px rgba(20, 20, 15, 0.03);
218 +}
219 +button.ctrl { cursor: pointer; transition: background 0.15s, border-color 0.15s; }
220 +button.ctrl:hover { background: var(--surface-2); }
221 +button.ctrl[aria-pressed="true"] {
222 + border-color: var(--series-1); color: var(--series-1);
223 + background: var(--accent-soft);
224 +}
225 +a.ctrl {
226 + background: var(--surface-1); border: 1px solid var(--border-strong);
227 + border-radius: 10px; padding: 8px 13px; font-size: 14px;
228 +}
229 +a.ctrl:hover { background: var(--surface-2); text-decoration: none; }
230 +
231 +.seg {
232 + display: inline-flex; border: 1px solid var(--border-strong);
233 + border-radius: 10px; overflow: hidden;
234 + box-shadow: 0 1px 2px rgba(20, 20, 15, 0.03);
235 +}
236 +.seg button {
237 + border: none; background: var(--surface-1); color: var(--text-secondary);
238 + padding: 8px 14px; font-size: 13.5px; font-weight: 500; cursor: pointer;
239 + font-family: inherit;
240 +}
241 +.seg button + button { border-left: 1px solid var(--border); }
242 +.seg button[aria-pressed="true"] {
243 + background: var(--series-1); color: #fff; font-weight: 600;
244 +}
245 +
246 +/* ------------------------------------------------------------------ badges */
247 +.badge {
248 + display: inline-flex; align-items: center; gap: 6px;
249 + border-radius: 999px; padding: 3px 11px; font-size: 12px; font-weight: 600;
250 + border: 1px solid var(--border); color: var(--text-secondary);
251 + background: var(--surface-1);
252 +}
253 +.badge .dot { width: 8px; height: 8px; border-radius: 50%; }
254 +
255 +.chip {
256 + display: inline-flex; align-items: center; gap: 7px;
257 + border: 1px solid var(--border-strong); border-radius: 999px;
258 + background: var(--surface-1); color: var(--text-primary);
259 + padding: 6px 13px; font-size: 13.5px; cursor: pointer;
260 + transition: border-color 0.15s, background 0.15s;
261 + font-family: inherit;
262 +}
263 +.chip:hover { border-color: var(--serious); background: var(--surface-2); }
264 +
265 +/* ------------------------------------------------------------------ tables */
266 +table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
267 +table.data th, table.data td {
268 + text-align: right; padding: 9px 12px; border-bottom: 1px solid var(--border);
269 + font-variant-numeric: tabular-nums;
270 +}
271 +table.data th:first-child, table.data td:first-child { text-align: left; }
272 +table.data th {
273 + color: var(--text-muted); font-weight: 600; font-size: 12px;
274 + letter-spacing: 0.05em; text-transform: uppercase;
275 +}
276 +table.data tr:last-child td { border-bottom: none; }
277 +table.data tbody tr { transition: background 0.12s; }
278 +table.data tbody tr:hover { background: var(--surface-2); }
279 +
280 +/* ---------------------------------------------------------------- headings */
281 +h1 {
282 + font-family: var(--font-display, serif);
283 + font-size: 30px; letter-spacing: -0.02em; margin: 30px 0 6px;
284 +}
285 +h2 {
286 + font-family: var(--font-display, serif);
287 + font-size: 21px; letter-spacing: -0.01em; margin: 34px 0 12px;
288 +}
289 +p.lede { color: var(--text-secondary); margin-top: 0; font-size: 15.5px; }
290 +
291 +.note { font-size: 13px; color: var(--text-muted); }
292 +.low-reliability-note {
293 + font-size: 13px; color: var(--text-secondary);
294 + background: var(--surface-2); border-radius: 10px; padding: 10px 14px;
295 + border-left: 3px solid var(--series-4);
296 + margin: 10px 0;
297 +}
298 +
299 +/* ------------------------------------------------------------ chart pieces */
300 +.chart-tooltip {
301 + background: var(--surface-1);
302 + border: 1px solid var(--border-strong);
303 + border-radius: 10px;
304 + padding: 9px 13px;
305 + font-size: 13px;
306 + box-shadow: var(--shadow-card-hover);
307 +}
308 +.chart-tooltip .tt-label { color: var(--text-muted); font-size: 12px; }
309 +.chart-tooltip .tt-value { font-weight: 700; font-size: 15px; }
310 +.chart-tooltip .tt-ci { color: var(--text-muted); font-weight: 400; font-size: 12.5px; }
311 +.chart-tooltip .tt-raw { color: var(--text-secondary); }
312 +.chart-tooltip .tt-n { color: var(--text-muted); font-size: 12px; }
313 +
314 +code {
315 + background: var(--surface-2);
316 + border-radius: 6px; padding: 1px 6px; font-size: 0.9em;
317 +}
318 +
319 +/* ------------------------------------------------------------- v3: pulse */
320 +button.primary, a.primary {
321 + background: var(--series-1); color: #fff; border: none;
322 + border-radius: 10px; padding: 9px 16px; font-size: 14px; font-weight: 600;
323 + cursor: pointer; display: inline-flex; align-items: center; gap: 8px;
324 + box-shadow: 0 2px 8px rgba(42, 120, 214, 0.3);
325 + transition: transform 0.15s, box-shadow 0.15s;
326 +}
327 +button.primary:hover, a.primary:hover {
328 + transform: translateY(-1px);
329 + box-shadow: 0 4px 14px rgba(42, 120, 214, 0.4);
330 + text-decoration: none;
331 +}
332 +
333 +table.pulse { width: 100%; border-collapse: collapse; font-size: 13.5px; }
334 +table.pulse th, table.pulse td {
335 + text-align: right; padding: 8px 10px; border-bottom: 1px solid var(--border);
336 + font-variant-numeric: tabular-nums; white-space: nowrap;
337 +}
338 +table.pulse th:first-child, table.pulse td:first-child { text-align: left; }
339 +table.pulse th {
340 + color: var(--text-muted); font-weight: 600; font-size: 11px;
341 + letter-spacing: 0.05em; text-transform: uppercase;
342 + background: var(--surface-1);
343 +}
344 +table.pulse tbody tr { cursor: pointer; transition: background 0.12s; }
345 +table.pulse tbody tr:hover { background: var(--surface-2); }
346 +td.heat { font-weight: 600; border-radius: 4px; }
347 +
348 +.record-chip {
349 + display: inline-flex; align-items: center; gap: 4px;
350 + font-size: 11px; font-weight: 700; color: var(--good);
351 +}
352 +
353 +.mini-tile { padding: 12px 14px; }
354 +.mini-tile .label { font-size: 10.5px; }
355 +.mini-tile .value { font-size: 20px; }
356 +.grid.cols-8 { grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr)); }
357 +
358 +/* ---------------------------------------------------------- v4 foundations */
359 +body, .card, .site-header, .site-footer, select, button.ctrl, a.ctrl,
360 +input, .seg button, .badge, .chip, table.pulse th {
361 + transition: background-color 0.25s ease, color 0.25s ease,
362 + border-color 0.25s ease, box-shadow 0.2s ease;
363 +}
364 +
365 +@keyframes qhpi-fade-up {
366 + from { opacity: 0; transform: translateY(10px); }
367 + to { opacity: 1; transform: none; }
368 +}
369 +.fade-up { animation: qhpi-fade-up 0.45s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
370 +.fade-up.d1 { animation-delay: 0.06s; }
371 +.fade-up.d2 { animation-delay: 0.12s; }
372 +.fade-up.d3 { animation-delay: 0.18s; }
373 +
374 +@keyframes qhpi-shimmer {
375 + 0% { background-position: 180% 0; }
376 + 100% { background-position: -20% 0; }
377 +}
378 +.skeleton {
379 + border-radius: 10px;
380 + background: linear-gradient(100deg, var(--surface-2) 40%,
381 + color-mix(in srgb, var(--border) 65%, var(--surface-2)) 50%,
382 + var(--surface-2) 60%);
383 + background-size: 200% 100%;
384 + animation: qhpi-shimmer 1.4s linear infinite;
385 +}
386 +
387 +@media (prefers-reduced-motion: reduce) {
388 + *, *::before, *::after {
389 + animation-duration: 0.01ms !important;
390 + animation-iteration-count: 1 !important;
391 + transition-duration: 0.01ms !important;
392 + }
393 +}
394 +
395 +.brand-mark { flex: none; display: inline-flex; align-self: center; }
396 +
397 +:focus-visible {
398 + outline: 2px solid var(--series-1);
399 + outline-offset: 2px;
400 + border-radius: 4px;
401 +}
402 +
403 +@media (max-width: 680px) {
404 + .container { padding: 0 14px 20px; }
405 + .site-header { padding: 10px 14px; gap: 8px; }
406 + .site-header nav a { padding: 5px 9px; font-size: 13px; }
407 + .hero { padding: 22px 0 4px; }
408 + .stat-tile .value { font-size: 25px; }
409 + .card { padding: 14px; border-radius: 13px; }
410 + .controls { gap: 8px; margin: 12px 0; }
411 + table.pulse { font-size: 12.5px; }
412 + table.pulse th, table.pulse td { padding: 6px 7px; }
413 + h1 { font-size: 25px; }
414 + h2 { font-size: 18px; }
415 +}
416 +
417 +/* -------------------------------------------------------------- v5: hot */
418 +/* Subtle film grain over the whole app */
419 +body::before {
420 + content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 1;
421 + opacity: 0.35; mix-blend-mode: overlay;
422 + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
423 +}
424 +main, header, footer { position: relative; z-index: 2; }
425 +
426 +/* Ticker tape */
427 +.ticker-shell {
428 + overflow: hidden; white-space: nowrap;
429 + border-bottom: 1px solid var(--border);
430 + background: var(--surface-1);
431 + margin: 0 -24px; padding: 7px 0; min-height: 36px;
432 +}
433 +.ticker-track { display: inline-flex; }
434 +.ticker-group {
435 + display: inline-flex; align-items: center;
436 + animation: ticker-scroll 55s linear infinite;
437 +}
438 +.ticker-shell:hover .ticker-group { animation-play-state: paused; }
439 +@keyframes ticker-scroll {
440 + from { transform: translateX(0); }
441 + to { transform: translateX(-100%); }
442 +}
443 +.ticker-item {
444 + display: inline-flex; align-items: baseline; gap: 7px;
445 + padding: 0 18px; font-size: 12.5px; color: var(--text-primary);
446 + border-right: 1px solid var(--border);
447 +}
448 +.ticker-item:hover { text-decoration: none; background: var(--surface-2); }
449 +.ticker-name { color: var(--text-secondary); font-weight: 600; }
450 +.ticker-val { font-variant-numeric: tabular-nums; font-weight: 700; }
451 +.ticker-item .delta { font-size: 11.5px; }
452 +
453 +/* Mega hero number */
454 +.hero-mega {
455 + display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
456 +}
457 +.hero-mega .big {
458 + font-family: var(--font-display, serif);
459 + font-size: clamp(52px, 7vw, 84px); font-weight: 700;
460 + line-height: 1; letter-spacing: -0.03em;
461 + background: linear-gradient(160deg, var(--text-primary) 55%, var(--series-1));
462 + -webkit-background-clip: text; background-clip: text;
463 + -webkit-text-fill-color: transparent;
464 +}
465 +.hero-mega .sub { font-size: 13.5px; color: var(--text-secondary); }
466 +
467 +/* Hero chart draw-in */
468 +.hero-chart { padding: 14px 14px 6px; overflow: hidden; }
469 +.hero-line {
470 + stroke-dasharray: 1; stroke-dashoffset: 1;
471 + animation: hero-draw 1.6s cubic-bezier(0.4, 0, 0.2, 1) 0.15s forwards;
472 +}
473 +.hero-area, .hero-band { opacity: 0; animation: hero-fade 0.8s ease 1.25s forwards; }
474 +.hero-dot { opacity: 0; animation: hero-fade 0.5s ease 1.6s forwards; }
475 +.hero-pulse {
476 + opacity: 0; transform-origin: center; transform-box: fill-box;
477 + animation: hero-fade 0.5s ease 1.6s forwards,
478 + hero-ping 2.4s ease-out 2.1s infinite;
479 +}
480 +@keyframes hero-draw { to { stroke-dashoffset: 0; } }
481 +@keyframes hero-fade { to { opacity: 1; } }
482 +@keyframes hero-ping {
483 + 0% { transform: scale(0.6); opacity: 0.45; }
484 + 70% { transform: scale(1.7); opacity: 0; }
485 + 100% { opacity: 0; }
486 +}
487 +
488 +/* Podium */
489 +.podium { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr)); }
490 +.podium .card { position: relative; overflow: hidden; }
491 +.podium .card::after {
492 + content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
493 +}
494 +.podium .p1::after { background: linear-gradient(90deg, #eda100, #ffd977); }
495 +.podium .p2::after { background: linear-gradient(90deg, #9a9a94, #d8d8d2); }
496 +.podium .p3::after { background: linear-gradient(90deg, #b0713d, #dda67a); }
497 +.podium .medal { font-size: 20px; }
498 +
499 +/* Command palette */
500 +.palette-backdrop {
501 + position: fixed; inset: 0; z-index: 90;
502 + background: color-mix(in srgb, var(--surface-0) 55%, transparent);
503 + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
504 + display: flex; align-items: flex-start; justify-content: center;
505 + padding: 12vh 16px 0;
506 + animation: qhpi-fade-up 0.18s ease both;
507 +}
508 +.palette {
509 + width: 100%; max-width: 560px;
510 + background: var(--surface-1); border: 1px solid var(--border-strong);
511 + border-radius: 16px; box-shadow: var(--shadow-card-hover);
512 + overflow: hidden;
513 +}
514 +.palette-input {
515 + width: 100%; border: none; outline: none;
516 + background: transparent; color: var(--text-primary);
517 + padding: 16px 18px; font-size: 16px; font-family: inherit;
518 + border-bottom: 1px solid var(--border);
519 +}
520 +.palette-list { list-style: none; margin: 0; padding: 6px; max-height: 320px; overflow-y: auto; }
521 +.palette-item {
522 + width: 100%; display: flex; justify-content: space-between; gap: 10px;
523 + border: none; background: none; text-align: left; cursor: pointer;
524 + padding: 10px 12px; border-radius: 10px; font-size: 14px;
525 + color: var(--text-primary); font-family: inherit;
526 +}
527 +.palette-item.active { background: var(--accent-soft); color: var(--series-1); }
528 +.palette-hint { color: var(--text-muted); font-size: 12px; }
529 +.palette-empty { padding: 14px; color: var(--text-muted); font-size: 13.5px; }
530 +.palette-foot {
531 + display: flex; gap: 16px; padding: 8px 16px;
532 + border-top: 1px solid var(--border);
533 + color: var(--text-muted); font-size: 11.5px;
534 +}
535 +.kbd-btn {
536 + display: inline-flex; align-items: center; gap: 6px;
537 + background: var(--surface-1); color: var(--text-secondary);
538 + border: 1px solid var(--border-strong); border-radius: 10px;
539 + padding: 6px 11px; font-size: 13px; cursor: pointer; font-family: inherit;
540 +}
541 +.kbd-btn:hover { background: var(--surface-2); }
542 +.kbd-btn kbd {
543 + background: var(--surface-2); border: 1px solid var(--border);
544 + border-radius: 5px; padding: 0 5px; font-size: 11px;
545 + font-family: inherit; color: var(--text-muted);
546 +}
547 +
548 +@media (max-width: 680px) {
549 + .ticker-shell { margin: 0 -14px; }
550 + .hero-mega { align-items: flex-start; }
551 +}
552 +
553 +/* -------------------------------------------------- v5.1: mobile polish */
554 +@media (max-width: 680px) {
555 + /* Compact header: brand + toggle on one row, nav scrolls on its own row */
556 + .brand small { display: none; }
557 + .kbd-btn { display: none; }
558 + .site-header { row-gap: 6px; }
559 + .site-header nav {
560 + order: 3; flex: 1 0 100%;
561 + flex-wrap: nowrap; overflow-x: auto;
562 + scrollbar-width: none; -webkit-overflow-scrolling: touch;
563 + margin: 0 -4px; padding: 0 4px;
564 + }
565 + .site-header nav::-webkit-scrollbar { display: none; }
566 + .site-header nav a { flex: none; }
567 +
568 + /* Hero: number below the title, full width */
569 + .hero { flex-direction: column; align-items: stretch; gap: 12px; }
570 + .hero-mega { align-items: flex-start; }
571 +
572 + /* Pulse table: drop secondary columns (1m, 6m, vs peak, $ value) —
573 + the full set remains in Explore's data table and the CSV/PDF exports */
574 + table.pulse th:nth-child(4), table.pulse td:nth-child(4),
575 + table.pulse th:nth-child(6), table.pulse td:nth-child(6),
576 + table.pulse th:nth-child(9), table.pulse td:nth-child(9),
577 + table.pulse th:nth-child(11), table.pulse td:nth-child(11) { display: none; }
578 +
579 + .controls input[type="text"] { flex: 1 1 160px; min-width: 0; }
580 + .controls input[type="range"] { flex: 1 1 110px; min-width: 0; }
581 + .ticker-item { padding: 0 12px; }
582 + .hero-chart { padding: 10px 8px 4px; }
583 + .primary, a.primary { padding: 8px 13px; font-size: 13.5px; }
584 + .palette-backdrop { padding: 8vh 10px 0; }
585 +}
586 +
587 +@media (max-width: 400px) {
588 + .stat-tile .value { font-size: 22px; }
589 + .mini-tile .value { font-size: 17px; }
590 + h2 { font-size: 17px; }
591 +}
added web/tsconfig.json +22 −0
@@ -0,0 +1,22 @@
1 +{
2 + "_header": "QWHPI — Quebec Weekly Housing Price Index | Author: Simon-Pierre Boucher | Contact: contact@spboucher.ai | File: web/tsconfig.json | Purpose: TypeScript configuration for the dashboard",
3 + "compilerOptions": {
4 + "target": "ES2022",
5 + "lib": ["dom", "dom.iterable", "esnext"],
6 + "allowJs": true,
7 + "skipLibCheck": true,
8 + "strict": true,
9 + "noEmit": true,
10 + "esModuleInterop": true,
11 + "module": "esnext",
12 + "moduleResolution": "bundler",
13 + "resolveJsonModule": true,
14 + "isolatedModules": true,
15 + "jsx": "preserve",
16 + "incremental": true,
17 + "plugins": [{ "name": "next" }],
18 + "paths": { "@/*": ["./*"] }
19 + },
20 + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
21 + "exclude": ["node_modules"]
22 +}
23