==========================
== Zhuo Hong Wei's Blog ==
==========================
Any and everything

HDB Resale Pricing

A few weeks ago, I started tinkering with our government’s HDB resale prices API . I had an idea to use recent transaction data to gauge if a listing on a property portal (e.g., property guru) is overpriced or fairly priced. Paralyzed by the sheer number of variables, I soon lost steam.

Resuming after a two week hiatus, I scoped the coding down to just fetching recent transactions which matched a listing’s address (specific block and street), flat type (4 room or 5 room) and floor level (low, mid and high).

One particular challenge I had was showing only transactions of a certain floor level. For example, a HDB resale property listing normally shows low, mid or high floor instead of exact floor for privacy reasons. The transaction data from the HDB resale prices API instead shows a range, something like “7 to 9”. According to this site, property agents normally classify low floor as below 5, mid floor as 5-8 and high floor as anything above 8.

Say I want to filter past transactions matching a high floor listing, for a past transaction record which has a floor range of “7 to 9”, should it be classified as mid or high?

I took the approach of converting the range into individual floors and counting if majority of it is high or mid. In this case 7 and 8 are mid and 9 is high so overally the transaction is counted as a mid floor transaction. Here’s the code that achieves that:

(defun atoi (s)
  (parse-integer s :junk-allowed t))

(defun trim (s)
  (string-trim '(#\Space) s))

(defun make-range (start end)
  (do ((x start (+ x 1)) (xs nil (cons x xs))) 
      ((> x end) (nreverse xs))))

(defun extract-floors (floor-range)
  (let ((boundaries (map 'list (alexandria:compose #'atoi #'trim) (split "TO" floor-range))))
      (if (every #'integerp boundaries) 
        (make-range (car boundaries) (cadr boundaries)))))

(defun floor-to-category (floor)
  (cond ((> floor 8) :high)
        ((> floor 4) :mid)
        (t :low)))

(defun make-floor-predicate (floor-category)
  (lambda (transaction)
    (let* ((floor-range (get-json-value "storey--range" transaction))
          (categories (map 'list #'floor-to-category (extract-floors floor-range)))
          (matches (count-if #'(lambda (x) (eq x floor-category)) categories))
          (non-matches (- (length categories) matches)))
      (> matches non-matches))))

Another challenge is how open ended the high floor category is. If any unit on the 9th floor and above are considered high floor, it would make no sense to gauge if a high floor listing is fairly priced if the listing is for a HDB block with 40 levels and the recent high floor transactions are in various ranges (10s, 20s, 30s) since we wouldn’t be able to know if the listing falls in which range. For that, I have no solution.

For now, I am contented with fetching closest past transactions. This is the sample output:

(hdb-resale-pricing::assess-listing :town "yishun" :block-number "315A" :flat-type "4 room" :floor-category :high :listing-price 500000)
2021-06 4 ROOM  13 TO 15        315A    YISHUN AVE 9    93 years 05 months      $500,000
2021-04 4 ROOM  10 TO 12        315A    YISHUN AVE 9    93 years 07 months      $472,000
2020-11 4 ROOM  13 TO 15        315A    YISHUN AVE 9    93 years 11 months      $473,000
2020-08 4 ROOM  13 TO 15        315A    YISHUN AVE 9    94 years 02 months      $435,000
2020-07 4 ROOM  10 TO 12        315A    YISHUN AVE 9    94 years 04 months      $436,000
2020-04 4 ROOM  13 TO 15        315A    YISHUN AVE 9    94 years 07 months      $432,000
2020-03 4 ROOM  10 TO 12        315A    YISHUN AVE 9    94 years 07 months      $428,000
2020-02 4 ROOM  10 TO 12        315A    YISHUN AVE 9    94 years 08 months      $423,888
2019-09 4 ROOM  13 TO 15        315A    YISHUN AVE 9    95 years 02 months      $426,000
2019-09 4 ROOM  13 TO 15        315A    YISHUN AVE 9    95 years 02 months      $440,000
2019-07 4 ROOM  13 TO 15        315A    YISHUN AVE 9    95 years 03 months      $412,000

The project also makes use of the json helper library which I wrote about in the previous post. The full source code for this project can be downloaded here.