How rare is your HDB flat?
Adjoined flats and Type S1 are about equally rare, 1 in 581 and 1 in 618 HDB resales, but one lives on a single street. Pick a flat model to see what it is, where it is and how many homes there really are.
Pick a flat model
Every section below changes to the model you pick. Commonest first, rarest last.
How rare is Type S1?
Each pennant is one HDB resale since January 2017. The big coloured ones are your model.
391 of 241,705 resales, January 2017 to September 2026
Each pennant is one resale, 2,000 of them. The big coloured ones are the Type S1 you would expect to meet: about 3.
Roughly 1 in 618 HDB resales since January 2017 was a Type S1.
What is Type S1?
In plain words, from what HDB's file shows. Press a name in the list to read another.
A flat's type is its room count: 1-room, 2-room, 3-room, 4-room, 5-room, Executive and Multi-Generation. Its model is HDB's name for the design, and the 4-room flats in the file alone come in 12 different models. These 10 are each less than 1 in 500 resales.
The 4-room flats in the 7 towers at blocks 1A to 1G Cantonment Road. At 50 floors, these are the tallest HDB blocks in Singapore. Type S2 is the 5-room version in the same towers.
All 10 names in plain words
- A maisonette is a home on two levels. This one is a 5-room flat, with leases starting 1983 to 1985, and it is bigger than other 5-room flats: typically 140 sqm against 117 sqm.5-room flats · Typically 140 sqm · Lease started 1983 to 1985
- Adjoined means joined together. The file does not say which flats were joined, but these homes are bigger than other flats of the same type: a typical Adjoined 4-room flat is 118 sqm, and other 4-room flats are 93 sqm. They come as 4-room, 5-room and Executive flats, with leases starting 1967 to 1989.4-room, 5-room and Executive flats · Typically 139 sqm · Lease started 1967 to 1989
- The 4-room flats in the 7 towers at blocks 1A to 1G Cantonment Road. At 50 floors, these are the tallest HDB blocks in Singapore. Type S2 is the 5-room version in the same towers.4-room flats · Typically 94 sqm · Lease started 2011
- The 5-room flats in the same 7 towers, blocks 1A to 1G Cantonment Road. Type S1 is the 4-room version.5-room flats · Typically 106 sqm · Lease started 2011
- A Premium Apartment with a loft. They come as 4-room and 5-room flats, with leases starting 2012 to 2016. A typical Loft 5-room flat is 147 sqm, against 117 sqm for other 5-room flats.4-room and 5-room flats · Typically 97 sqm · Lease started 2012 to 2016
- HDB homes in low, two-storey blocks, not in a tall block of flats. Every Terrace resale was in a two-storey block on one of four streets: Jalan Bahagia, Jalan Ma'mor, Jalan Tenteram and Stirling Road. They come as 3-room and 4-room homes, with leases starting 1968 to 1972. A typical Terrace 3-room home is 94 sqm, against 67 sqm for other 3-room flats.3-room and 4-room flats · Typically 98 sqm · Lease started 1968 to 1972
- An older multi-generation flat, and not the same label as 3Gen. The file gives it its own flat type, Multi-Generation, and its leases started 1987 to 1988, decades before 3Gen's 2018 to 2021.Multi-Generation flat type · Typically 164 sqm · Lease started 1987 to 1988
- Short for three generations. HDB describes 3Gen flats as for eligible multi-generation families living under one roof. In the file they are 5-room flats, with leases starting 2018 to 2021, and the first resale was in 2022.5-room flats · Typically 120 sqm · Lease started 2018 to 2021
- Another maisonette label (a home on two levels). In the file it is a 5-room flat, with leases starting 1983, sold in 4 blocks.5-room flats · Typically 137 sqm · Lease started 1983
- A maisonette label again, this time for Executive flats. Leases started 1997 to 1998, and it was sold in 14 blocks in 2 towns.Executive flats · Typically 147 sqm · Lease started 1997 to 1998
Where can you find Type S1?
Every block and street with a resale since January 2017, counted once per block.
All 391 resales were at 7 blocks on one street, Cantonment Road, in Central Area.
Which rare models are easiest to find?
Number of blocks with at least one resale, since January 2017. Your pick is highlighted.
Adjoined flat sold in 322 blocks across 23 towns. Type S1, almost as rare, sold in 7 blocks on one street.
How many homes are there really?
Resales count sales, not homes. Where HDB's block list lets us count the homes, here are both.
For every 100 Type S1 homes there were about 32 resales since January 2017. For the average HDB flat it is about 22.
Type S1 homes are sold about 1.5 times as often as the average flat, so it looks more common in resales than it is in homes: 1 in 618 resales, but 1 in 904 homes.
How we counted the homes: The 4-room flats in the 7 blocks 1A to 1G Cantonment Road.
How often does Type S1 come up?
Resales per year. 2020 and 2021 were affected by COVID-19: across all flats, resales peaked in 2021 at 29,087.
About 40 a year overall. Roughly steady: 39 a year in 2017 to 2019, 36 a year in 2024 to 2026.
Fun finds
The short version
Adjoined flats (416 resales, 1 in 581) and Type S1 (391, 1 in 618) are about equally rare, but Adjoined flats sold in 322 blocks across 23 of 26 towns and Type S1 in 7 blocks on 1 street, from HDB's file of 241,705 resales from January 2017 to September 2026, downloaded 8 Oct 2026.
What we did. We looked at the 10 flat models that are each less than 1 in 500 HDB resales. Together they are 1 in 126 resales (1,916 of 241,705). For each we counted resales, blocks, towns and streets, and read what the file shows about its rooms, size and lease start to explain the name in plain words. HDB does not publish homes by model, so for 4 of the 10 we counted homes from HDB's block list.
What we found. 3Gen, Multi Generation and Terrace are the rarest of the 8 models with at least 50 resales (1 in 2,877, 1 in 2,686 and 1 in 1,965). Improved-Maisonette and Premium Maisonette have fewer than 50 resales, so they are shown but not ranked. Type S1 and Type S2 are the 4-room and 5-room flats in the 7 50-floor towers on Cantonment Road. 3Gen's leases start 2018 to 2021, and it had no resales in 2017 to 2019.
What to keep in mind. A resale count is not a count of homes. Terrace is 1 in 1,965 resales but about 1 in 3,908 homes: 285 homes, of which 43 per 100 changed hands in 9 years 9 months, against 22 per 100 for the average flat. HDB's file gives model names but no definitions, so the plain meanings come from what the file shows, apart from the everyday meanings of maisonette and adjoined, and HDB's own line on 3Gen.
By Ben (opens in a new tab). Updated . Data: HDB, Resale flat prices based on registration date from Jan-2017 onwards and HDB Property Information, data.gov.sg.
Questions people ask
What are the different HDB flat models?
The resale file lists 21 flat models. The commonest are Model A (87,206 resales), Improved (58,982 resales), New Generation (29,280 resales), Premium Apartment (26,427 resales) and Simplified (9,222 resales). The 10 rarest, each less than 1 in 500 resales, are Model A-Maisonette, Adjoined flat, Type S1, Type S2, Premium Apartment Loft, Terrace, Multi Generation, 3Gen, Improved-Maisonette and Premium Maisonette. This page explains those 10.
What is the difference between a flat type and a flat model?
The flat type is the room count the file uses: 1-room, 2-room, 3-room, 4-room, 5-room, Executive and Multi-Generation. The flat model is HDB's name for the design. The same flat type comes in many models: the 4-room flats in the file alone come in 12 different models.
What are Type S1 and Type S2 HDB flats?
The 4-room flats in the 7 towers at blocks 1A to 1G Cantonment Road. At 50 floors, these are the tallest HDB blocks in Singapore. Type S2 is the 5-room version in the same towers. Since January 2017 there have been 391 Type S1 resales (1 in 618) and 199 Type S2 resales (1 in 1,215), all at the same 7 blocks.
What does 3Gen mean for an HDB flat?
Short for three generations. HDB describes 3Gen flats as for eligible multi-generation families living under one roof. In the file they are 5-room flats, with leases starting 2018 to 2021, and the first resale was in 2022. It is rare in resales because it is new to them: no resales in 2017 to 2019 and 58 in 2024 to 2026, so far 84 in all, 1 in 2,877.
What is a Multi Generation flat, and is it the same as 3Gen?
No, the file keeps them as separate labels. An older multi-generation flat, and not the same label as 3Gen. The file gives it its own flat type, Multi-Generation, and its leases started 1987 to 1988, decades before 3Gen's 2018 to 2021. There have been 90 Multi Generation resales since January 2017 (1 in 2,686), and HDB's block list shows 367 multi-generation homes in 8 blocks.
What is an HDB Terrace house, and how many are there?
HDB homes in low, two-storey blocks, not in a tall block of flats. Every Terrace resale was in a two-storey block on one of four streets: Jalan Bahagia, Jalan Ma'mor, Jalan Tenteram and Stirling Road. They come as 3-room and 4-room homes, with leases starting 1968 to 1972. A typical Terrace 3-room home is 94 sqm, against 67 sqm for other 3-room flats. We count 285 Terrace homes: every home in the 41 two-storey blocks on those streets. HDB's block list does not name models, so this count is ours, not HDB's. Since January 2017 there have been 123 Terrace resales, 1 in 1,965 of all resales.
What is an Adjoined flat, and are Adjoined flats rare?
Adjoined means joined together. The file does not say which flats were joined, but these homes are bigger than other flats of the same type: a typical Adjoined 4-room flat is 118 sqm, and other 4-room flats are 93 sqm. They come as 4-room, 5-room and Executive flats, with leases starting 1967 to 1989. By resales they are rare: 1 in 581. But they are not hard to find: they were sold in 322 blocks across 23 of 26 towns, and 249 of those blocks had just one resale. Type S1 is almost as rare (1 in 618) but sits in 7 blocks on one street.
What are the maisonette flat models?
A maisonette is a home on two levels. The file has three maisonette labels among the rare models: Model A-Maisonette (420 resales), Improved-Maisonette (32) and Premium Maisonette (22). Model A-Maisonette is a 5-room flat, with leases starting 1983 to 1985, and it is bigger than other 5-room flats: typically 140 sqm against 117 sqm.
What is a Premium Apartment Loft?
A Premium Apartment with a loft. They come as 4-room and 5-room flats, with leases starting 2012 to 2016. A typical Loft 5-room flat is 147 sqm, against 117 sqm for other 5-room flats. There have been 139 resales since January 2017, 1 in 1,739.
What are the rarest HDB flat models?
Among the 8 models with at least 50 resales since January 2017, the rarest by share of resales are 3Gen (1 in 2,877), Multi Generation (1 in 2,686) and Terrace (1 in 1,965). Improved-Maisonette (32 resales) and Premium Maisonette (22) are rarer still, but too few to rank. A share of resales is not the same as a share of homes.
Does 1 in 1,965 mean 1 in 1,965 homes?
No. The 1 in 1,965 counts resales. Terrace homes are sold about twice as often as the average flat (43 resales per 100 homes in 9 years 9 months, against 22), so there are fewer Terrace homes than the resale share suggests: 1 in 3,908. Type S1 is sold about 1.5 times as often as the average flat (32 per 100), so 1 in 618 resales is 1 in 904 homes.
Which rare flat models are in my town?
Pick your town in the "Where can you find it?" section above. It lists every rare model with a resale in that town since January 2017.
The numbers behind it
| Flat model | Resales | Share of resales | Blocks | Towns | Status |
|---|---|---|---|---|---|
| Model A-Maisonette | 420 | 1 in 575 | 37 | 9 | Ranked |
| Adjoined flat | 416 | 1 in 581 | 322 | 23 | Ranked |
| Type S1 | 391 | 1 in 618 | 7 | 1 | Ranked |
| Type S2 | 199 | 1 in 1,215 | 7 | 1 | Ranked |
| Premium Apartment Loft | 139 | 1 in 1,739 | 19 | 2 | Ranked |
| Terrace | 123 | 1 in 1,965 | 37 | 2 | Ranked |
| Multi Generation | 90 | 1 in 2,686 | 8 | 3 | Ranked |
| 3Gen | 84 | 1 in 2,877 | 31 | 9 | Ranked |
| Improved-Maisonette | 32 | 1 in 7,553 | 4 | 1 | Fewer than 50 resales, not ranked |
| Premium Maisonette | 22 | 1 in 10,987 | 14 | 2 | Fewer than 50 resales, not ranked |
| Flat model | Flat type | Typical size (sqm) | Leases started | Where |
|---|---|---|---|---|
| Model A-Maisonette | 5-room | 140 | 1983 to 1985 | 37 blocks in 9 towns |
| Adjoined flat | 4-room, 5-room and Executive | 139 | 1967 to 1989 | 322 blocks in 23 towns |
| Type S1 | 4-room | 94 | 2011 | 7 blocks on Cantonment Road |
| Type S2 | 5-room | 106 | 2011 | 7 blocks on Cantonment Road |
| Premium Apartment Loft | 4-room and 5-room | 97 | 2012 to 2016 | 19 blocks in 2 towns |
| Terrace | 3-room and 4-room | 98 | 1968 to 1972 | 37 blocks in 2 towns |
| Multi Generation | Multi-Generation | 164 | 1987 to 1988 | 8 blocks in 3 towns |
| 3Gen | 5-room | 120 | 2018 to 2021 | 31 blocks in 9 towns |
| Improved-Maisonette | 5-room | 137 | 1983 | 4 blocks in 1 town |
| Premium Maisonette | Executive | 147 | 1997 to 1998 | 14 blocks in 2 towns |
| Flat model | Resales | Share of resales | Flat types in the file |
|---|---|---|---|
| Model A | 87,206 | 1 in 3 | 2-room, 3-room, 4-room and 5-room |
| Improved | 58,982 | 1 in 4 | 1-room, 2-room, 3-room, 4-room and 5-room |
| New Generation | 29,280 | 1 in 8 | 3-room and 4-room |
| Premium Apartment | 26,427 | 1 in 9 | 2-room, 3-room, 4-room, 5-room and Executive |
| Simplified | 9,222 | 1 in 26 | 3-room and 4-room |
| Apartment | 8,632 | 1 in 28 | Executive |
| Maisonette | 6,598 | 1 in 37 | Executive |
| Standard | 6,398 | 1 in 38 | 2-room, 3-room, 4-room and 5-room |
| DBSS | 3,823 | 1 in 63 | 2-room, 3-room, 4-room and 5-room |
| Model A2 | 2,708 | 1 in 89 | 4-room |
| 2-room | 513 | 1 in 471 | 2-room |
| Model A-Maisonette | 420 | 1 in 575 | 5-room |
| Adjoined flat | 416 | 1 in 581 | 4-room, 5-room and Executive |
| Type S1 | 391 | 1 in 618 | 4-room |
| Type S2 | 199 | 1 in 1,215 | 5-room |
| Premium Apartment Loft | 139 | 1 in 1,739 | 4-room and 5-room |
| Terrace | 123 | 1 in 1,965 | 3-room and 4-room |
| Multi Generation | 90 | 1 in 2,686 | Multi-Generation |
| 3Gen | 84 | 1 in 2,877 | 5-room |
| Improved-Maisonette | 32 | 1 in 7,553 | 5-room |
| Premium Maisonette | 22 | 1 in 10,987 | Executive |
| Flat model | Homes counted | Share of homes | Share of resales | Resales per 100 homes | Against the average flat |
|---|---|---|---|---|---|
| Type S1 | 1,232 | 1 in 904 | 1 in 618 | 32 | about 1.5 times as often as |
| Type S2 | 616 | 1 in 1,808 | 1 in 1,215 | 32 | about 1.5 times as often as |
| Terrace | 285 | 1 in 3,908 | 1 in 1,965 | 43 | about twice as often as |
| Multi Generation | 367 | 1 in 3,035 | 1 in 2,686 | 25 | about as often as |
| Average HDB flat | 1,113,907 | - | - | 22 | - |
| Flat model | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
|---|---|---|---|---|---|---|---|---|---|---|
| Model A-Maisonette | 31 | 33 | 46 | 37 | 62 | 55 | 39 | 50 | 35 | 32 |
| Adjoined flat | 31 | 42 | 46 | 49 | 50 | 40 | 43 | 40 | 37 | 38 |
| Type S1 | 41 | 39 | 37 | 24 | 72 | 42 | 37 | 30 | 38 | 31 |
| Type S2 | 23 | 24 | 16 | 18 | 42 | 23 | 11 | 15 | 14 | 13 |
| Premium Apartment Loft | 1 | 2 | 4 | 26 | 43 | 13 | 10 | 10 | 12 | 18 |
| Terrace | 12 | 12 | 14 | 15 | 19 | 13 | 7 | 14 | 8 | 9 |
| Multi Generation | 4 | 15 | 14 | 8 | 18 | 12 | 3 | 6 | 4 | 6 |
| 3Gen | 0 | 0 | 0 | 0 | 0 | 5 | 21 | 21 | 19 | 18 |
| Improved-Maisonette | 5 | 6 | 2 | 2 | 3 | 2 | 5 | 3 | 2 | 2 |
| Premium Maisonette | 2 | 1 | 4 | 3 | 1 | 3 | 0 | 1 | 4 | 3 |
| All flats | 20,509 | 21,561 | 22,186 | 23,333 | 29,087 | 26,720 | 25,754 | 27,832 | 25,084 | 19,639 |
The boring bit
Where the data comes from
HDB's Resale flat prices based on registration date from Jan-2017 onwards, on data.gov.sg: 242,259 rows, downloaded 8 Oct 2026. We used the 241,705 from January 2017 to September 2026. For homes we used HDB's HDB Property Information, on data.gov.sg: 13,392 block records and 1,113,907 flats sold by HDB.
What is not claimed
- A resale is not a home. HDB gives a block and street but no unit, so one flat sold twice counts twice. Only 4 models have a count of homes, and those counts are ours (see below).
- The resale file leaves out sales that may not reflect the full market price, such as sales between relatives and sales of part shares (HDB's own note), so every share here is a share of those resales.
- Homes counted: Type S1 is the 4-room flats and Type S2 the 5-room flats in the 7 blocks 1A to 1G Cantonment Road (every resale in those blocks is S1 or S2). Terrace is every home in the 41 two-storey blocks on Jalan Bahagia, Jalan Ma'mor, Jalan Tenteram and Stirling Road (all 37 blocks with a Terrace resale are two-storey blocks there, and no other model was sold in those 41 blocks). Multi Generation is the multi-generation units HDB lists in 8 blocks. HDB's block list does not name flat models, so these are our matches.
- Adjoined flat, Premium Apartment Loft, Model A-Maisonette, 3Gen and the two other maisonette models sit inside blocks with other flats, so their homes cannot be counted from the block list.
- What the names mean: HDB's file lists model names but does not define them. The plain words on this page come from what the file shows (flat type, floor area, lease start, number of floors, streets). "Typical size" is the middle (median) resale. Two meanings are everyday word meanings, not HDB definitions: a maisonette is a home on two levels, and adjoined means joined together. The 3Gen line follows HDB's own description of 3Gen flats on its Types of flats page (hdb.gov.sg), read 8 Oct 2026.
- Premium Apartment Loft and 3Gen have only recently started to resell, so their shares reflect a young model.
- The window is January 2017 to September 2026, which includes 2020 and 2021, years affected by COVID-19. No figure on this page is for a single year.
- Models with fewer than 50 resales are shown but not ranked.
- Model names are HDB's own. "Multi Generation" and "3Gen" are separate labels in the file, so we keep them separate.
- HDB's block list says it runs to 31 Dec 2025, yet it lists 77 residential blocks completed in 2026. We use it as published.
- This says nothing about how good, valuable or comfortable any flat model is.
Coverage
Every resale from January 2017 to September 2026 was read: 241,705 of 241,705. The file also holds 554 rows for October 2026, a month not yet finished, which we left out. Every record in the block list was read: 13,392 of 13,392.
Report an error. Corrections are logged publicly with a date.
How we did it
- Downloaded HDB's two files from data.gov.sg on 8 Oct 2026: 242,259 resale rows and 13,392 block records.
- Kept January 2017 to September 2026: 241,705 resales. The 554 rows for October 2026, a month not yet finished, are left out.
- Counted each rare model: resales, blocks (a block number on a street), towns, streets, and resales per year.
- Ranked only models with at least 50 resales. The ten models are those that are each less than 1 in 500 resales.
- Counted homes where the block list allows it: Type S1 and S2 (the 4-room and 5-room flats in blocks 1A to 1G Cantonment Road), Terrace (every home in the two-storey blocks on the four Terrace streets) and Multi Generation (the multi-generation units the list shows). The code stops if any resale of those models is outside those blocks.
- Counted twice. The prep script counts with pandas and the page counts again with plain Python; the build stops if any model's resales, blocks, towns or resales per year, or any count of homes, differs.
Show the Python code
"""Rebuilds this insight's numbers from HDB's two public files and writes data/flat_models.json.
python prep/prep_flat_models.py download both files from data.gov.sg, then rebuild
python prep/prep_flat_models.py --no-download rebuild from the two files already in data/raw/ (no download)
Files (in data/raw/):
ResaleFlatPrices2017.csv Resale flat prices based on registration date from Jan-2017 onwards (data.gov.sg)
HDBPropertyInformation.csv HDB Property Information (data.gov.sg)
Also written: data/dataset_metadata.json (data.gov.sg's own record of each file: name, last updated, coverage end).
Window: every resale from Jan 2017 to Sep 2026. Oct 2026 is a month not yet finished, so it is left out.
A model is ranked only if it has at least THRESH resales; the others are shown but not ranked.
The page counts the same numbers again with plain Python (page.py); build_page.py stops if the two disagree.
"""
import json, math, re, sys, time, urllib.request
from pathlib import Path
import pandas as pd
HERE = Path(__file__).resolve().parent
DATA = HERE.parent / "data"
RAW = DATA / "raw"
DATASETS = {"resale": ("d_8b84c4ee58e3cfc0ece0d773c8ca6abc", "ResaleFlatPrices2017.csv"),
"property": ("d_17f5382f26140b1fdae0ba2ef6239d2f", "HDBPropertyInformation.csv")}
THRESH = 50
LAST = "2026-09"
MODELS = ["Model A-Maisonette", "Adjoined flat", "Type S1", "Type S2", "Premium Apartment Loft", "Terrace",
"Multi Generation", "3Gen", "Improved-Maisonette", "Premium Maisonette"]
ABBR = {"AVE": "Avenue", "ST": "Street", "RD": "Road", "DR": "Drive", "CRES": "Crescent", "CTRL": "Central", "STH": "South",
"NTH": "North", "BT": "Bukit", "CL": "Close", "GDN": "Garden", "GDNS": "Gardens", "JLN": "Jalan", "LOR": "Lorong",
"PL": "Place", "TER": "Terrace", "UPP": "Upper", "TG": "Tanjong", "KG": "Kampong", "C'WEALTH": "Commonwealth",
"HTS": "Heights", "PK": "Park", "LK": "Link", "BLVD": "Boulevard", "MKT": "Market", "CTR": "Centre"}
SPECIAL = {"MCNAIR": "McNair", "MA'MOR": "Ma'mor", "QUEEN'S": "Queen's"}
def get(url):
# data.gov.sg refuses requests that carry no User-Agent (HTTP 403), so send a plain one
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (CompareSG insight prep)"})
with urllib.request.urlopen(req, timeout=120) as r:
return r.read()
def download():
keep_all = {}
RAW.mkdir(parents=True, exist_ok=True)
for key, (ds, fname) in DATASETS.items():
meta = json.loads(get("https://api-production.data.gov.sg/v2/public/api/datasets/" + ds + "/metadata"))["data"]
keep_all[key] = {k: meta.get(k) for k in ("datasetId", "name", "managedBy", "lastUpdatedAt", "coverageStart", "coverageEnd", "format")}
for _ in range(20):
j = json.loads(get("https://api-open.data.gov.sg/v1/public/api/datasets/" + ds + "/poll-download"))["data"]
if j.get("status") == "DOWNLOAD_SUCCESS":
break
time.sleep(3)
(RAW / fname).write_bytes(get(j["url"]))
(DATA / "dataset_metadata.json").write_text(json.dumps(keep_all, indent=1), encoding="utf-8")
if "--no-download" not in sys.argv:
download()
def ru(x):
return int(math.floor(x + 0.5))
def pretty(s):
out = []
for w in str(s).split():
out.append(ABBR.get(w) or SPECIAL.get(w) or (w if any(c.isdigit() for c in w) else w.capitalize()))
return " ".join(out)
def town(t):
return {"KALLANG/WHAMPOA": "Kallang/Whampoa"}.get(t, t.title())
r_all = pd.read_csv(RAW / "ResaleFlatPrices2017.csv")
raw_rows = len(r_all)
r = r_all[r_all.month <= LAST].copy()
N = len(r)
months = (2026 - 2017) * 12 + 9
years = months / 12
r["town"] = r.town.map(town)
r["street"] = r.street_name.map(pretty)
r["blk"] = r.block.astype(str)
r["key"] = r.blk + "|" + r.street
r["flat_type"] = r.flat_type.str.title().str.replace(" Room", " room", regex=False)
r["year"] = r.month.str[:4].astype(int)
YRS = [str(y) for y in range(2017, 2027)]
all_per_year = [int((r.year == y).sum()) for y in range(2017, 2027)]
assert sum(all_per_year) == N
towns_all = sorted(r.town.unique())
p = pd.read_csv(RAW / "HDBPropertyInformation.csv")
sold_cols = [c for c in p.columns if c.endswith("_sold")]
p["sold"] = p[sold_cols].sum(axis=1)
stock_total = int(p.sold.sum())
p["street_p"] = p.street.map(pretty)
p["blk"] = p.blk_no.astype(str)
p["key"] = p.blk + "|" + p.street_p
avg_per_home = N / stock_total # resales per home over the whole window, for the average flat
out = {"N": N, "months": months, "years": years, "stock_total": stock_total, "last_full": LAST, "thresh": THRESH, "yrs": YRS,
"all_per_year": all_per_year, "median_area_all": float(r.floor_area_sqm.median()), "raw_rows": raw_rows,
"oct_rows": int((r_all.month > LAST).sum()), "prop_rows": len(p), "n_towns_all": len(towns_all), "towns_all": towns_all,
"avg_per_home": avg_per_home}
# ---- exact duplicates (same row twice) : counted, not removed, because HDB gives no unit number ----
dup = r.duplicated(subset=["month", "town", "flat_type", "blk", "street_name", "storey_range", "floor_area_sqm", "flat_model",
"lease_commence_date", "resale_price"], keep=False)
out["dups_all"] = int(dup.sum())
models = []
for name in MODELS:
x = r[r.flat_model == name]
n = len(x)
g = x.groupby("key")
blocks = g.ngroups
blocks_once = int((g.size() == 1).sum())
# ties are broken by name, so the order does not depend on the pandas version
tl = x.groupby("town").agg(n=("key", "size"), b=("key", "nunique"))
tl = tl.loc[sorted(tl.index, key=lambda t: (-tl.n[t], -tl.b[t], t))]
sl = x.groupby("street").size()
sl = sl.loc[sorted(sl.index, key=lambda t: (-sl[t], t))]
py = [int((x.year == y).sum()) for y in range(2017, 2027)]
first3, last3 = sum(py[:3]), sum(py[-3:])
ramp = (last3 / (33 / 12)) / max(first3 / 3, 0.0001) > 3 or first3 == 0
types = sorted(x.flat_type.unique(), key=lambda s: (s == "Executive", s))
# same-type comparison: median area of this model vs every other resale of the same flat type
cmp = []
for t in types:
a = x[x.flat_type == t].floor_area_sqm
b = r[(r.flat_type == t) & (r.flat_model != name)].floor_area_sqm
if len(a) >= 20 and len(b) >= 20:
cmp.append([t, float(a.median()), float(b.median()), int(len(a))])
m = dict(name=name, n=int(n), one_in=N / n, blocks=int(blocks), blocks_once=blocks_once, towns=int(len(tl)), streets=int(len(sl)),
town_list=[[t, int(v.n), int(v.b)] for t, v in tl.iterrows()], street_list=[[s, int(v)] for s, v in sl.items()],
first3=first3, last3=last3, ramp=bool(ramp), types=types, area=[float(x.floor_area_sqm.min()), float(x.floor_area_sqm.max())],
area_med=float(x.floor_area_sqm.median()), lease=[int(x.lease_commence_date.min()), int(x.lease_commence_date.max())],
per_year=py, first_year=int(x.year.min()), ranked=bool(n >= THRESH), cmp=cmp, stock=None)
models.append(m)
byname = {m["name"]: m for m in models}
# ---- homes ("stock"), only where HDB's block list lets us count them ----
# Type S1 and S2: blocks 1A-1G Cantonment Road
pin = p[(p.street_p == "Cantonment Road") & p.blk.isin(list("1" + c for c in "ABCDEFG"))]
assert len(pin) == 7 and (pin.residential == "Y").all()
pin_keys = set(pin.key)
rp = r[r.key.isin(pin_keys)]
assert set(rp.flat_model) <= {"Type S1", "Type S2"}, set(rp.flat_model)
assert set(r[r.flat_model.isin(["Type S1", "Type S2"])].key) == pin_keys
s1_homes, s2_homes = int(pin["4room_sold"].sum()), int(pin["5room_sold"].sum())
assert int(pin[["1room_sold", "2room_sold", "3room_sold", "exec_sold", "multigen_sold", "studio_apartment_sold"]].sum().sum()) == 0
assert set(rp[rp.flat_model == "Type S1"].flat_type) == {"4 room"} and set(rp[rp.flat_model == "Type S2"].flat_type) == {"5 room"}
out["pin_blocks"] = 7
out["pin_floors"] = int(pin.max_floor_lvl.max())
assert (pin.max_floor_lvl == out["pin_floors"]).all()
out["pin_tallest_in_file"] = bool(out["pin_floors"] == p[p.residential == "Y"].max_floor_lvl.max())
# Terrace: every home in two-storey residential blocks on the four streets that have Terrace resales
tr = r[r.flat_model == "Terrace"]
tstreets = sorted(tr.street.unique())
tp = p[(p.street_p.isin(tstreets)) & (p.residential == "Y") & (p.max_floor_lvl == 2)]
assert set(tr.key) <= set(tp.key), "a Terrace resale is outside the two-storey blocks"
assert set(p[p.key.isin(set(tr.key))].max_floor_lvl) == {2}
t_homes = int(tp.sold.sum())
other_in_tp = set(r[r.key.isin(set(tp.key))].flat_model) - {"Terrace"}
out["terrace_stock_blocks"] = int(len(tp))
out["terrace_streets"] = tstreets
out["terrace_other_models_in_those_blocks"] = sorted(other_in_tp)
out["terrace_towns"] = sorted(tr.town.unique())
out["terrace_max_area"] = float(tr.floor_area_sqm.max())
# Multi Generation: units HDB lists as multigen
mgp = p[p.multigen_sold > 0]
mg = r[r.flat_model == "Multi Generation"]
assert set(mg.key) <= set(mgp.key), "a Multi Generation resale is outside the blocks that list multigen units"
mg_homes = int(mgp.multigen_sold.sum())
out["mg_blocks"] = int(len(mgp))
for name, homes in [("Type S1", s1_homes), ("Type S2", s2_homes), ("Terrace", t_homes), ("Multi Generation", mg_homes)]:
m = byname[name]
m["stock"] = homes
m["stock_one_in"] = stock_total / homes
m["per_home"] = m["n"] / homes
m["x_avg"] = m["per_home"] / avg_per_home
# ---- extras used in copy ----
out["all_rare_n"] = int(sum(m["n"] for m in models))
out["n_models_all"] = int(r.flat_model.nunique())
out["flat_types"] = sorted(r.flat_type.unique())
out["blocks_2026"] = int(((p.year_completed == 2026) & (p.residential == "Y")).sum())
out["n_models_4room"] = int(r[r.flat_type == "4 room"].flat_model.nunique())
am = r.groupby("flat_model").agg(n=("key", "size"), t=("flat_type", lambda s: sorted(set(s))))
am = am.loc[sorted(am.index, key=lambda k: (-am.n[k], k))]
out["all_models"] = [[k, int(v.n), v.t] for k, v in am.iterrows()]
out["models"] = models
out["adj_once_pct"] = ru(100 * byname["Adjoined flat"]["blocks_once"] / byname["Adjoined flat"]["blocks"])
json.dump(out, open(DATA / "flat_models.json", "w"), indent=1, default=float)
print("N", N, "stock_total", stock_total, "dups", out["dups_all"])
for m in models:
print(f"{m['name']:24} n={m['n']:5} 1in{m['one_in']:8.0f} blocks={m['blocks']:4} towns={m['towns']:2} streets={m['streets']:3} types={m['types']} area={m['area']} med={m['area_med']} lease={m['lease']} stock={m['stock']} cmp={m['cmp']}")
print("terrace stock blocks", out["terrace_stock_blocks"], t_homes, "other models in those blocks", other_in_tp)
print("pin floors", out["pin_floors"], "tallest in file", out["pin_tallest_in_file"], "S1/S2 homes", s1_homes, s2_homes, "mg", mg_homes, out["mg_blocks"])
print("models total", out["n_models_all"], "4room models", out["n_models_4room"])
What could make it wrong: a model name or block entered wrongly in HDB's files, a block that holds a mix of models (which would make a count of homes too high), or a plain-words line that does not match how HDB itself describes a model. The two separate counts agree on every model, so the counting is not the weak point; the files are.
No machine learning was needed. The questions were "how many" and "where", which counting answers directly.
Source: HDB, Resale flat prices based on registration date from Jan-2017 onwards (opens in a new tab) and HDB Property Information (opens in a new tab), data.gov.sg, downloaded 8 Oct 2026. Contains information from data.gov.sg made available under the Singapore Open Data Licence.