我本來以為這是個週末專案。
內政部的不動產實價登錄是公開資料,每旬(每月 1、11、21 日)釋出批次檔,涵蓋民國 101 年第 3 季至今、約 14 年、22 個縣市。我想做的事很單純:把它變成一張能查的表。結果這個「單純」的目標,讓我把 4,824,149 筆買賣交易翻過來又翻過去,最後隔離率壓到 0.094% 才算勉強滿意。
這篇記錄我踩到的坑。如果你也打算自己處理這份資料,希望能幫你少走一點路。
## 一、表面上的坑:一眼就看得到的那種
下載下來先是一堆分縣市、分期別的 zip,每個 CSV 打開都有**兩列表頭**——第一列中文、第二列英文。你要是直接 `read_csv`,第一筆「資料」會是英文表頭。
再來是日期。`1130520` 不是西元,是民國 113 年 5 月 20 日,得 `+1911`。樓層更妙,「移轉層次」寫的是中文:「十八層」「地下二層」「全」。你得自己把中文數字解析成整數,還要處理「地下」變負數、「全」對應到總樓層。
這些都煩,但都是機械性的煩,寫一次就過了。真正讓我停下來想很久的,是下面這些。
## 二、六碼日期之謎:資料自己證明了假說
清洗交易日期時,絕大多數是七碼 `YYYMMDD`,但冒出一批**六碼**的,像 `990728`。
第一反應:髒資料,丟掉。但數量不對——有四千多筆,集中在最早那幾期。丟掉之前我多看了一眼:`990728`,如果補一個 0 變成 `0990728`,就是民國 99 年 7 月 28 日,一個完全合理的日期。這些是把民國年寫成兩位數的舊格式。
問題是,交易日期怎麼會早於制度上線(2012,民國 101 年)?一筆「民國 99 年」的交易出現在後來才釋出的批次裡,說得通嗎?
我做了一件事驗證:把這批補 0 救回的列(民國 90–99,共 4,128 筆)拿出來,比對它們的「交易日期」與「建築完成日期」。結果是——**97%(3,302 / 3,415)的建築完成日晚於交易日**。
這一刻我知道假說對了:這些是**預售屋**。買方簽約在前(交易日),房子蓋好移轉登記在後(完工日),而申報時填的交易日期是當年的原始契約日。所以一筆民國 99 年簽約、102 年完工的預售屋,會帶著「民國 99 年」的交易日,出現在後來的批次裡。資料不是髒,是我一開始沒讀懂。
最後的處理:六碼補 0 還原,只接受還原後民國年落在 [90, 99] 且月日有效的;還原不了的(342 筆)標記 `R-03-YYMM-AMBIG` 隔離,不硬湊日精度。民國 90–94 的尾端只有 124 筆(佔全庫 0.0026%),不是雜訊叢集,就留著。
## 三、備註欄的語意陷阱:字面比對會出事
備註欄是這份資料的靈魂,也是地雷。它記載了特殊交易——親友間交易、法拍、瑕疵屋、持分……這些交易的價格通常偏離市場,統計時得標記出來甚至排除。
最直覺的做法是關鍵字比對:備註含「海砂屋」就標記為瑕疵。但我差點掉進一個坑:有些備註寫的是「賣方不負物之瑕疵擔保責任,**海砂、輻射、兇宅除外**」。這句話的意思是「這間**不是**海砂屋」,你要是命中「海砂」就標記,剛好標反。
我的裁決方式是做「免責句式分析」:統計某個詞出現在**正向斷言**還是**除外免責句**的比例。實測「海砂屋」有 96%、「輻射屋」有 97% 是正向陳述(直接說這間是),只有約 2% 落在除外句。於是我用帶「屋」字的 `海砂屋`/`輻射屋` 當關鍵字,剛好避開那句「海砂、輻射……除外」的免責樣板。
另一個兩義的詞是「夾層」。夾層可能是合法登記的樓層,也可能是灌進坪數、拉高單價的違建增建。我一樣做了分析:27,129 筆命中裡,明確否定的(無夾層、夾層除外)只有 16 筆(0.06%),99.94% 是正向陳述,而且多半跟「增建」「頂樓」同時出現。所以我把它歸到「未登記」類。
至於那些出現在 8–9% 交易裡的「土地及建物分件登記案件」之類——那是登記結構的樣板文字,不是特殊交易,不給任何標籤。分得清哪些是雜訊、哪些是訊號,才是這欄的重點。
## 四、為什麼你不能直接拿官方 CSV 算中位數
這是我覺得最值得講的一點。
全庫有 **18.51%** 的交易被標記為特殊交易。這些交易系統性地偏離市場價——親友交易通常偏低、含車位捆綁的坪價被稀釋。如果你直接拿官方原始資料算某區的中位數,等於把這些雜訊一起算進去。
具體有多少差?拿樣本(臺北市 + 板橋,最近四季)裡「大安區・住宅大樓」的每坪中位數:
- 直接算(含特殊交易):1,074,759 元/坪
- 排除 `is_special` 之後:1,144,507 元/坪
差 **6.09%**,大約每坪 7 萬元。你以為你在看市場行情,其實被親友交易之類的低價案往下拉了。(全庫尺度同計約 5.5%。)車位捆綁也一樣:板橋排除車位捆綁的列之後,每坪中位數上修約 3.15%。
這不是要你信我的數字——這正是我把清洗欄位(`is_special`、`special_tags`、`parking_bundled`)全部留在資料裡的原因。你可以自己排除、自己驗算。
## 五、工程紀律(一段就好)
處理這種會迭代的清洗規則,我守三條:**隔離不丟棄**(任何被剔除的列都進 quarantine、可回放,不靜默消失)、**冪等重建**(同一份原始檔重跑,資料庫內容雜湊不變)、**逐期自動驗收**(56 期每期跑列數恆等、主鍵唯一、孤兒率等檢查)。
老實說,這三條不是我一開始就做好的。我曾經有個 bug:主檔最終型別轉換用了 strict cast,某一列的房間數是 3,333,333(明顯是誤植)撐爆了 int16,整個轉換拋例外,然後被外層的 try/except 靜默略過——**整個檔案**被跳過。三個檔案、約三萬列就這樣無聲消失,還連帶讓它們的明細變成八萬多筆「孤兒」。是驗收裡的孤兒率異常把它抓出來的。修法是把 cast 改成非 strict(溢位變 NULL、保留該列),並讓任何 transform 例外整檔進 quarantine 而不是消失。
我把這段寫出來,是因為「隔離不丟棄」這條紀律的價值,恰恰是在它幫我抓到自己的 bug 時才真正體現。
## 六、成果與後續
最後這份資料:4,824,149 筆買賣、56 期無缺、隔離率 0.094%、每旬更新(官方發布後 T+1,**非即時**)。
我把臺北市全區 + 板橋、最近四季共 16,857 筆整理成一個可以直接跑的樣本 repo,JSONL 和 CSV 都有,附資料字典和上面那幾個範例的可執行程式碼:
- 樣本 repo:`https://openlvr.lewayone.workers.dev/`
- 完整 14 年、22 縣市、API 與整包資料集還在做,有興趣可以留 email:`https://tally.so/r/KYZAZz`
聲明一下:這是我自己做的第三方加值,**不是**內政部官方服務,跟內政部沒有任何合作或授權關係。資料就是官方的公開資料,我只是把坑填一填。
如果你也在跟這份資料搏鬥,歡迎交流。