A six-repo series on Singapore's public data · 1 of 6

4-room resale price per m² dipped in Q3 2026 vs a year earlier: −1.3% on the headline median (S$6,647 → S$6,559/m²) and −0.6% on the mean basis used for the decomposition — both point the same way. Town-share mix nets to ≈0: rate −53.3 S$/m², mix −0.7, interaction +13.3. “Rate” still includes what sold within each town: Central Area (23 sales in each quarter) contributes −16.4 of that −53.3, coinciding with fewer Cantonment Road Type S1 sales (15 → 8). Restricting the split to towns with ≥25 sales in each quarter moves the rate to −38.5. A small dip; not a like-for-like flat-price estimate; one quarter.

−1.32%
national 4-room median price/m², Q3 2026 vs Q3 2025 (S$6,647 → 6,559)
16 of 23
shown towns lower — Queenstown +10.9% to Bukit Batok −6.3% (towns need ≥25 sales in each quarter to appear)
rate −53.3 · mix −0.7
S$/m², transaction-weighted means — interaction +13.3 reported separately, not folded in
−0.6% … +1.0%
the total across 3/6/12-month windows — small, but the sign changes (12 months: +0.987%)
4-room median price per m² by town — Q3 2026 vs Q3 2025
Median price/m² by town — Q3 2025 in grey; Q3 2026 in teal if higher and burnt orange if lower. Every town is in the CSV; charts show towns with ≥25 sales in each quarter.

More views

Rolling 3-month medians of town price per m²
Median of the last three monthly medians, 2023–2026 — not pooled sales
Town-mix drift over the sample
Town-mix drift over the sample
Rate/mix waterfall of the national move
The rate/mix waterfall

The Power BI page

The Power BI report page for this dataset
The Power BI report page built from the same pipeline — the .pbix ships in the GitHub release.

Method

  1. Pull — HDB resale flat prices from data.gov.sg (241,822 rows × 11 columns in the historical 2026-10-02 snapshot; manifest time is a file-modification-time proxy, not a direct download receipt; the raw file is never edited). src/download.py
  2. Audit — profile the data first; rules are set before any analysis. docs/data_audit.md
  3. Stage & check — parse and clean with every exclusion counted; 8 assertions, including non-empty staged data, fail loudly. sql/01 · sql/05
  4. Measure & decompose — town×month medians and a rolling median of the last three monthly medians (not all sales pooled; missing months supply no median), then a shift-share split of the per-m² move into rate / mix / interaction (on means — medians are not additive). sql/03 · sql/04
  5. Draw, write, present — figures are code, in light and dark (src/figures.py); then the decision memo and the Power BI page.

Reproduce

Requires uv. Run in Bash (Linux/macOS or Git Bash on Windows):

git clone https://github.com/faizsaifulnizam/hdb-resale-mart && cd hdb-resale-mart
uv venv .venv --python 3.12          # or: python -m venv .venv
if [ -f .venv/Scripts/activate ]; then
  source .venv/Scripts/activate      # Windows Git Bash
else
  source .venv/bin/activate          # Linux / macOS
fi
uv pip install -r requirements.txt   # or: pip install -r requirements.txt

python src/download.py       # raw CSV → data/raw/ (gitignored)
python src/build_dataset.py  # staging + 8 checks → data/processed/sales.parquet
python src/analysis.py       # medians, YoY, decomposition, sensitivity → outputs/
python src/figures.py        # re-renders reports/figures/

Then check outputs/town_4room_yoy.csv: Queenstown reads 10,666.67 → 11,833.33, and the national medians match the headline. Data as of the 2026-10-02 pull — a later re-pull can move the newest months.

Limits

4-room flats only; price is normalized by floor area, but flat-size mix is not decomposed. The split uses means, not medians, and does not reconstruct the official HDB price index. Town-average “rate” still includes block, storey, lease and model composition; the Central Area sales pattern is descriptive, not causal proof. All 26 towns remain in the national split; only the chart suppresses the three below-threshold towns. Registrations can revise; this is not a forecast.

Read more