R 技術文 — 區議會選區分界歷屆變動及人口地圖

Eric
Eric
May 4 · 11 min read

上文用簡單例子講點用 R 製作區議會選區地圖,相信唔少人(點解會咁天真覺得有唔少人睇呢啲技術文) 到喉唔到肺,今日深入少少,首先講吓用 leaflet 繪製地圖嘅常用進階選項,再講點解讀 PDF 檔同埋將人口數據加返落各個選區。

(完整即食原始碼放喺各章節底,已合併前文相關代碼,唔駛前抄後抄咁辛苦)

A. 區議會選區分界變遷

上文提到點用 leaflet 繪製一年嘅 DCCA 分界地圖,而依家我地有齊 2003 年至今各屆選區分界數據,想進一步比較唔同選舉年嘅分界,又可以點做呢?

如果有玩開 Photoshop 之類東西,相信對圖層 (Layer) 呢個概念唔會陌生,而喺 leaflet 一樣用到類似概念。簡而言之,可以睇成將印有不同內容嘅透明膠片疊起來,而你就要決定哪些放頂,哪些放底,咁有啲內容就會遮住見唔到,而喺 leaflet ,會用 zIndex 去表達放置不同 layers 嘅先後次序,數值越大就越接近表面,說明文件建議數值應介乎 400 與 500 之間。

建立設置不同 zIndex 的 MapPane 後,將不同年度的選區分界按次序加至相應的 MapPane,然後再設定外觀(如顏色、透明度、邊框粗幼等等)。當然少不了為選區加上名稱標籤,以及與鼠標互動的反白功能,不然定必會給四百多個選區弄得一頭霧水。最後就加上 LayersControl,供用家決定顯示哪些年度的分界,方便比較選區分界歷年變遷 (例如究竟會否有些疑似 gerrymandering 的小動作呢?)

比較 2019、2015 年選區分界
完整原始碼 (Part A)

B. 區議會選區人口偏離標準人口基數幅度地圖

標題水蛇春咁長,1999 咁,睇完都唔知講乜,唔緊要,唔明講到你明為止。現行選區標準人口基數為 16,599人,法例規定選區人口應盡量接近基數,並且唔應該偏離基數超過 25%,故此政府會不時重新劃分甚或增刪選區,但實際上不少選區嘅偏離幅度礙於諸多因素遠超 25%,而 2019 年區議會選區預計人口可參閱選管會文件 (按此)。究竟邊區偏離基數多啲?除左睇足 18 頁紙,可唔可以一張地圖睇晒呢?

B1. 處理 PDF 檔及清理數據

一打開,又係討厭嘅 PDF 檔,所以今日會順道講吓點處理 PDF 檔。傳說要成為檸檬廚神有十大要訣,例如一般人單手做嘅,就反手做,而一般人用工具做嘅,就用返手做。雖然網上有唔少快靚正轉換 PDF 做 Excel 檔嘅工具,為咗向檸檬致敬,小弟就自討苦吃拎 R 嘅 tabulizer package 嚟做示範點處理 PDF 檔 (完全費時失事,仲要人手後期執)。

拆解 PDF 檔其實只得一行 code:

# READ THE PDF FILE AND EXTRACT THE TABLES
# https://www.eac.hk/pdf/distco/2019dc/final/ch/Appendix_VI(Chi).pdf
out_mat = extract_tables("Appendix_VI(Chi).pdf", method="stream", encoding='UTF-8')

然後就會抽到 18 區各區嘅表格出嚟,但真係三尖八角、岩岩巉巉,個個 table 嘅欄數都唔同,點搞呀?(話咗你用網上啲工具咪好囉,攞苦嚟辛)

若然仔細檢查每個表格,會發現頭兩個 columns 同最尾兩個 columns 有用,那就先把呢啲有用 columns 抽出嚟。先用 regular expression (i.e. Column 1 有 valid DCCA 編號, e.g. A01) 選取有用嘅 rows ,之後再將 Column 2 (中英文名) 分開返做中文、英文名兩個 columns,而 Column 3 人口數字就要移除非數字符號,然後移除埋 Column 4 嘅百分比符號,最後再將 18 個 tables 合埋做一個 dataframe 就接近大功告成了。

## CREATE AN EMPTY DATA FRAME
out_df = data.frame()## READ EACH OF THE 18 TABLES
for (k in 1:18){
  ## EXTRACT THE RELEVANT COLUMNS (NOTE: NUMBER OF COLUMNS VARIES)
  out_temp = out_mat[[k]][,c(1,2,ncol(out_mat[[k]])-1,ncol(out_mat[[k]]))]
  
  ## USE REGULAR EXPRESSION TO EXTRACT THE RELEVANT ROWS (WITH VALID DCCA ID)
  out_temp = out_temp[grep("\\D\\d\\d", out_temp[,1], perl=TRUE),]
  
  ## SPLIT THE NAME INTO 2 COLUMNS (CHINESE NAME AND ENGLISH NAME) 
  out_temp = cbind(out_temp, str_match(as.character(out_temp[,2]),"([^\\s]+)\\s+(.+)")[,c(2:3)])
  
  ## CONVERT TO A DATAFRAME
  out_temp = as.data.frame(out_temp, stringsAsFactors=FALSE)
  
  ## CONVERT THE POPULATION TO INTEGER (REMOVE NON-NUMERIC CHAR)
  out_temp[,3] = as.integer(gsub("[^0-9]","",out_temp[,3]))
  
  ## CONVERT THE PERCENTAGE TO NUMERIC TYPE
  out_temp[,4] = as.numeric(as.numeric(sub("%","",out_temp[,4])))
  
  ## COMBINE THE DATAFRAME WITH THE CONSOLIDATED ONE
  out_df = rbind(out_df, out_temp)
}## EXPORT THE CONSOLIDATED DATAFRAME TO AN EXCEL FILE
## CLEAN THE FILE IN THE EXCEL
## EDIT DCCAs E17, F25 (>1 ROW IN THE NAME COLUMN)
write.xlsx(out_df, "out.xlsx", row.names=FALSE)## IMPORT THE CLEANED CONSOLIDATED EXCEL FILE
imp_df = read.xlsx("out.xlsx", sheetIndex = 1, encoding="UTF-8")## RENAME THE COLUMNS
colnames(imp_df) = c("CACODE","NAME","Pop","Deviation","CNAME","ENAME")## DROP UNNECESSARY COLUMNS
imp_df['NAME'] <- NULL
imp_df['ENAME'] <- NULL

點解係接近大功告成?因為本身 PDF 檔有兩個 DCCAs 名稱太長以致佔咗兩行,結果解讀時出現錯誤,由於懶得 hard code,直接匯出至 Excel 檔再人手改返啱佢就算,嗰兩個 DCCAs 分別係 E17 同 F25。匯入檔案後再加返 Column 名,移除埋唔要嘅 Columns 就叫做整理好呢堆數據了。(再次提大家,真係上網 convert 算啦)

B2. 載入 Shapefile 以及將人口數據併入

之後就係大家熟口熟面嘅步驟,將 2019 年區議會選區 Shapefile 匯入 (下刪一百字……睇返前文吧)

####################################################################
## IMPORT THE SHAPEFILE
DC2019 = readOGR(dsn='DCCA_2019.shp')## SET THE CRS (COORDINATE REF SYSTEM) TO HK1980 GRID SYSTEM (EPSG:2326)
## https://spatialreference.org/ref/epsg/2326/
projection(DC2019) = crs("+init=epsg:2326")## TRANSFORMATION INTO WGS 84 SYSTEM (EPSG:4326)
## https://spatialreference.org/ref/epsg/4326/
DC2019 = spTransform(DC2019,"+init=epsg:4326")## REMOVE "CNAME", THE SAME AS THE "ENAME" IN THE 2019 DCCA FILE
DC2019@data$CNAME <- NULL

大家可以望吓 DC2019 呢舊野嘅構造,分做@ data 同埋 @ polygons,前者載住啲名呀、編號,後者就係載住分界座標,下一步就係將頭先千辛萬苦清理好嘅人口數據加入去 @ data 那個 dataframe。left_join() 又出現了 (可參見另文 Section 3), 以 “CACODE” 作為 Key 就可以將兩個 dataframes 合體。然後再定義 palette function (參見另文 Section 4),根據輸入數值 map 返色塊顏色。

## ADD THE ADDITIONAL DATA TO THE DC2019 SpatialPolygonsDF USING LEFT_JOIN
DC2019@data = left_join(DC2019@data, imp_df,by=c("CACODE"="CACODE"))## CREATE A PALETTE FUNCTION
pal <- colorNumeric(
  palette = "PiYG",
  domain = DC2019@data$Deviation, reverse = TRUE)

B3. 用 leaflet 繪製地圖

最後當然係要將所有野顯示喺地圖上面,同前文比,呢段 code 長咗好多,逐舊慢慢解釋:

i) Basemap 選項

呢度介紹埋如果想俾用家自己選擇不同底圖,其實可以事先放唔同 provider 嘅參數去 call addProviderTiles(),之後再加上 addLayersControl() 即可。

用 OpenStreetMap 做底圖

ii) 依偏離基數人口嘅幅度填色,再加上標籤等等

資料標籤

最麻煩應該係標籤,因為要分行以及加上格式,所以要用到 HTML 語法 (e.g. <b>, <br>),paste() 會將括號內的東西加上空白 (或在 sep 選項另外指定)串埋,例如 paste(“Apple”, “Pen”) 會變成 “Apple Pen”,而 paste0() 就唔會加間隔字符,例如 paste0(“Apple”, “Pen”) 就會變成 “ApplePen”,最重要係要指明呢堆野係 HTML 碼,而非普通嘅 String。之後加埋 highlightOptions(),鼠標滑到選區之上就會反白整個選區,再彈出剛才加上嘅資料標籤。最後可以喺 RStudio 將地圖匯出成 Webpage,方便大家同其他人分享。

一理通百理明,搞掂呢兩篇文,應該已經可以玩到好多人口普查嘅分區數據,如果你有其他 shapefiles,其實步驟都係類似嘅啫。

完整原始碼 (Part B)

資料來源: 香港政府為原始資料的版權擁有人,使用條款請瀏覽 data.gov.hk

最後更新:2019.05.05

Eric

Written by

Eric

八十後香港廢青