Election Results Visualization with ggplot2

Dennis Tseng
Dennis, R, data, news
34 min readMar 19, 2020
ggplot2 畫的總統大選各縣市得票率 faceted plot

用 R 語言畫幾種常見的選舉圖表!

你也可以在我用 R 搭建的部落格上讀到這篇文章
我在 Taiwan R User Group 分享的簡報 還有 Github 程式碼

Introduction

想像一下,如果你想知道國民黨總統候選人 2020年大選在各縣市的表現如何,會怎麼開始呢?

最直覺的作法就是直接找出他們在各縣市的得票資料,而資料的長相就像底下的圖一樣,台北市幾票、台南市幾票。

## # A tibble: 5 x 6
## place number party name vote per
##
## 1 臺北市 2 中國國民黨 推薦 韓國瑜張善政 685830 0.420
## 2 新北市 2 中國國民黨 推薦 韓國瑜張善政 959631 0.389
## 3 桃園市 2 中國國民黨 推薦 韓國瑜張善政 529113 0.404
## 4 臺中市 2 中國國民黨 推薦 韓國瑜張善政 646366 0.381
## 5 臺南市 2 中國國民黨 推薦 韓國瑜張善政 339702 0.291

因為上面是簡單的表格,雖然直接看得到得票數跟票數占比,但除了得票資料以外,還需要一張以縣市劃分界線的台灣地圖,讓你可以依照得票高低,把對應到或深或淺的顏色填入各個縣市當中。但是,以 dataframe 型態儲存的得票資料,要怎麼「畫」到地圖裡面呢?一個可行的方法是上網找台灣的 SVG ,匯入 Illustrator 後把顏色逐一填入。但如果不只要畫國民黨總統候選人的得票率,連不分區和區域立委選舉也都要一起,甚至,如果你想畫到村里等級的得票率,這就是程式派上用場的好時機了。

用 R 或者其他程式語言畫圖表有幾個優點,第一個優點是正確性,使用程式取代人為填色,可以大幅減少出錯的可能性,以不分區 19 個政黨各自的各縣市得票率地圖,就有 19 * 20 (6直轄 + 11縣 + 3市),一共有380個格子要填,稍有不慎就會看錯數字;第二個優點則是可重複利用性,資料有變化時不用重畫,直接套用前寫好的程式碼就好,應用情境像是跟著開票進度更新圖表;第三個優點則是速度,選好程式碼執行就好,不用再逐一顏色。其實,上面說的的正確、可重複利用、速度,綜合起來就是偷懶,因為自己在 Illustrator 裡面填色填到快瘋掉,所以才決定研究怎麼用 R 實現快速畫出選舉圖表。

文章開始,我會先介紹 R 當中地理資料的常見型態,接下來則會以台灣為例子,帶大家走過一遍取得台灣縣市/鄉鎮/村里圖資,並將資料引入 R 的流程。有了地理圖資以後,我會用 2020 台灣總統與立委選舉的的得票資料為範例,示範如合畫出以下的圖表類型:背景地圖/Background Map、統計地圖/Choropleth、點示地圖/Dot Distribution Map or Bubble Map、示意地圖/Cartogram、國會席次圖/Parliament Plot、六邊型網格圖/Hexmap or Tilegram。

這篇文章改寫自今年年初我在 Taiwan R User Group 的分享,若有錯誤,尤其是地理學相關,請一定幫我指出,以免誤人子弟。另外因為篇幅原因,所以這篇文章不會包含常見的散點圖/Scatterplot、堆疊長條圖/Stacked Bar Plot 等,有興趣的人可以 yahoo/google “ggplot2 tutorial”為了方便讀者參考,我把檔案上傳到 Github repo 上,raw data 和程式碼都在裡面,底下截了幾張分享時的簡報。

左邊是端傳媒的 Choropleth,右邊是 Cartogram
左邊是 Dot Distribution Map,右邊是端傳媒上面的 Hexmap
左邊是呈現 swing 的 Hexmap ,右邊是關鍵評論網的議會席次圖

部分圖表類型的分類與程式範例參考 R Graph Gallery 以及其他資源,並著重呈現以台灣為範例的呈現。跟著這篇文章走過一遍後,畫出來的圖表可以直接輸出成 PNG/JPG,也可以另外輸出 SVG 到其他軟體後製。就讀者而言,這篇文章預設的讀者是對 R 語言有一定基礎、使用過 ggplot2 套件,並且想知道如何繪製更多樣圖表的人,相對來說則較不適合想聽選情分析,或是想產出設計師等級圖表的人。底下正文開始!

R and Spatial Data

在這個章節,我會分別介紹 R 當中的地理資料型態、地理資料的原始格式、如何取得台灣的地理資料並匯入 R 語言。

Spatial Data in R: sf

在 R 當中,常見的一種空間資料/地理資料(以下將混用空間與地理兩個詞彙)型態是 sf ,它結合了地理特徵與非地理特性,地理特徵像是點、線、多邊形、點與線的組合、線與線的組合,或者是這幾個元素混合在一起,非地理特徵像是地理區的名稱、編號,或者是其他紀錄了人口數量、行政區類型等的欄位。sf 的長相則跟常見的 dataframe 一樣,只是 dataframe 當中地理特性以 list columns 的形式儲存。在 R 裡面處理 sf 資料的套件就是 library(sf),官網中有一系列教學

其實,除了 sf 以外,若上網查詢在 R 中應該如何處理地理資料,會看到很多人使用 library(sp),sp 是另一種常見的空間資料型態,只是與 sp 相比,sf 的資料型態易懂且好操作,譬如說可以直接用 st_join() 串接地理資料,而且生態系完整,像是 sf 物件也支援 tidyverse 底下 library(dplyr) 的操作,再加上清楚易懂的官方教學,讓 library(sf) 逐漸熱門,也因此逐漸取代 sp。若對 sp 與 sf 有興趣,可以參考 Should I learn sf or sp for spatial R programming? 還有Spatial R – Moving from SP to SF,另外,Geocompuations with R 裡面介紹 sf 的段落也值得一看,書中直接提到 "sf largely supersedes the sp ecosystem.",因此本文才主要介紹 sf。

an example of sf data

Spatial Data in R: raster

raster (網格資料) 的長相和 sf 有很大的不同,它由數個 cells/pixels (格子)所組成,每個格子都代表一個區域,而格子都有自己對應到的值,拿 raster 資料來繪圖時,格子當中值的大小可以表現出不同的顏色或是單色的深淺,藉此表示不同的地理特徵,像是地勢高低或是土地類型。若對 raster 資料有興趣,Intro to Raster Data in R 有大量的插圖幫助理解。

因為應用學門不同,資料型態也不同,所以這次教學不會使用 library(raster),將會以 library(sf)為主,底下還是有稍微呈現一下在 R 中 raster 資料的長相,以及利用 plot() 畫出的圖。

# aut <- raster::getData("alt", country = "AUT", mask = TRUE)
# aut %>% write_rds(str_c(dirpath_data, "geo_raw/aut.rds"))
aut <- read_rds(str_c(dirpath_data, "geo_raw/aut.rds"))
aut
## class : RasterLayer
## dimensions : 336, 948, 318528 (nrow, ncol, ncell)
## resolution : 0.008333333, 0.008333333 (x, y)
## extent : 9.4, 17.3, 46.3, 49.1 (xmin, xmax, ymin, ymax)
## crs : +proj=longlat +datum=WGS84 +ellps=WGS84 +towgs84=0,0,0
## source : D:/GitHub/Dennis_R_Data_News/content/post/AUT_msk_alt.grd
## names : AUT_msk_alt
## values : 108, 3533 (min, max)
plot(aut)

上面介紹了 R 當中兩種常見的地理資料型態 — sf & raster,也就是 sf & 底下則要帶大家看,import 進 R 之前,這些地理資料的原始長相是什麼,底下會以 GeoJSON 和 Shapefile 為主。

Raw Spatial Data: GeoJSON

GeoJSON 是一種結合地理特徵與非地理特性的 JSON 檔案,你可以用 library(geojsonR) 讀取 GeoJSON,套件的官網有一篇教學告訴你要如何處理 GeoJSON 資料。將 GeoJSON 匯入到 R 裡面之後,他會以 nested lists 的型態儲存,我們可以透過函數再將它轉成 sf 物件。

另外,有一種資料型態叫做 TopoJSON,它是 GeoJSON 的延伸,因為能夠有效率的消除冗餘(redundancy),因此可以減少原始檔案的大小。可以參考topojson 的 github 網站

an example of GeoJSON

Raw Spatial Data: Shapefile

Shapefile 是一種非常普及的開放空間資料格式,已經被大量使用多年,也有不少 GIS 軟體支援,上網搜尋台灣的地理圖資,有很多資料就是用 Shapefile 的形式儲存。若想了解更多,可以參考 Open and Plot Shapefiles in R

Raw Spatial Data: kml

KML 是一種奠基於 xml 形式且用途廣泛的地理資料格式,舉例來說,可以將 KML 資料匯入到 Google Earth 裡面,另外,它可以儲存特徵/變數,也可以保有 raster 元素,可以參考 What is KML?

Taiwan Geographical Data

上面談了 R 當中的空間資料型態,以及空間資料的原始長相,底下則是要帶大家一起取得台灣地理資料並匯入到 R 裡面。步驟其實很簡單:下載原始資料、簡化資料、輸入至 R 環境中。

Get Taiwan Data

想獲取台灣的地理資料有許多來源,也有不同的資料格式可以使用。以台灣的縣市為例子,政府資料開放平臺提供了直轄市與縣市界線的圖資鄉鎮市區行政區域界線圖資全國村里界線,下載便可以看到 Shapefile 檔案;此外,code for america 也有提供台灣的 GeoJSON 檔案;也有人準備了這份臺灣 GIS 資料來源整理。這邊會以政府資料開放平台的縣市界線圖資作為例子。

Simplify Data

以村里界線的原始資料來說,若在解壓縮後將 Shapefile 引入到 R 裡面,需要花上很多時間,而且因為檔案略大的緣故,操作也不太順利,因此在這裡參考Data Man 的資料視覺化筆記的介紹,你可以利用 mapshaper 工具簡化檔案。

mapshaper 的上傳檔案頁面
mapshaper 的上傳檔案頁面

以上面提過的縣市界線圖資來說,下載後你會看到一個 .zip 檔案,解壓縮後有 .dbf, .shx, .shp, .prj 等多個檔案,但是先別急著刪除 .zip ,因為 mapshaper 支援直接上傳 .zip 檔,上傳後你會看到網站如下圖。

上傳 .zip 檔案後的長相

點擊 import 後,可以看到以鄉鎮市為界線的台灣地理圖資,接著點擊右上角的 Simplify ,依照自己的需求選擇要勾選的項目後,就可以調整想要簡化的比例了。可以將簡化的比例拉到最右端的 0% ,再來往左端慢慢增加簡化的 % 數,在簡化檔案大小與表達清楚之間取得平衡。

import 後的畫面長相
按下 simplify 之後可以左右拉動調整簡化比例

簡化完畢後,點擊 Export ,可以看到好幾種檔案格式,除了前面介紹過的 Shapefile, GeoJSON, TopoJSON 以外,也有 csv 與 SVG 的選擇。底下的示範會以 Shapefile 和 GeoJSON 為主,所以可以先下載這兩種檔案。

按下 export 之後可以看到有數種格式讓你選擇

如果你對新聞業實際在畫地理圖表前如何處理資料有興趣,可以參考前面提過的 Data Man 所寫的 天下雜誌2018台灣選舉地圖製作分享:技術的部分。接下來就可以開始畫圖表了!

Visualizing Election Results with R

底下先載入需要的套件,接著就要開始畫圖了:

# Package names
packages <- c('tidyverse','sf','jsonlite','ggparliament','maps',
'geojson','geojsonio','geojsonsf','cartogram')
# Install packages not yet installed
installed_packages <- packages %in% rownames(installed.packages())
if (any(installed_packages == FALSE)) {
install.packages(packages[!installed_packages])
}
# Packages loading
invisible(lapply(packages, library, character.only = TRUE))
# devtools::install_github("olihawkins/clhex")
library(clhex)

Background Map

第一個要介紹的圖表類型非常簡單,就是背景地圖,你可看到我先讀取了剛剛下載得到的 Shapefile 與 GeoJSON 檔案,再將它們分別讀入,讀檔後利用 geom_sf() 畫圖。不同類型的圖表大部分都以背景地圖為基底,加上其他的特徵例如人口數或者分區得票率。

### read simplified shp
sf_county_shp_small = st_read(str_c(dirpath_data, "geo_raw/COUNTY_MOI_1081121/COUNTY_MOI_1081121.shp"))
## Reading layer `COUNTY_MOI_1081121' from data source `D:\GitHub\Dennis_R_Data_News\static\data\2020-03-15-election-results\geo_raw\COUNTY_MOI_1081121\COUNTY_MOI_1081121.shp' using driver `ESRI Shapefile'
## Simple feature collection with 22 features and 4 fields
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: 116.7166 ymin: 20.69729 xmax: 123.4891 ymax: 26.383
## epsg (SRID): NA
## proj4string: +proj=longlat +ellps=GRS80 +no_defs
sf_county_shp_small %>%
ggplot() + geom_sf()

上面讀了剛剛下載下來的 Shapefile,畫圖結果沒什麼大問題,若你在意背景怎麼灰灰的,底下會告訴你應該怎麼調整。

### read geojson
county_json_raw = read_json(str_c(dirpath_data, "geo_raw/tawian_county.json"))
sf_county_geojson_simplified = county_json_raw %>% as.json() %>% geojson_sf() %>%
mutate(COUNTYNAME = iconv(COUNTYNAME, "UTF-8", "BIG-5") )
sf_county_geojson_simplified %>%
ggplot() + geom_sf()
### add longitude and latitude constraint
sf_county_geojson_simplified %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot() + geom_sf()

這部分則是讀了 GeoJSON,如大家所見,因為第二張地圖涵蓋了離島,所以台灣本島不很清楚,因此我利用了st_crop()加上了經緯度的限制,將繪圖的範圍限制到台澎金馬,畫圖的效果就好很多了。後續的其他圖表會以這邊的 sf_county_geojson_simplified 為基礎。

Choropleth

第二個要介紹的圖表類型是統計地圖,它是一種依照變數數值替不同尺度的區域上色的地圖,舉例來說,台北市各行政區的人口密度圖就是一種統計地圖,這裡所關照的變數是人口密度,而區域的尺度則是行政區。

底下使用預先計算好的 2020 總統得票作為範例,繪製出以縣市為尺度的國民黨得票率統計地圖。將 raw data 輸入到 R 當中後,我篩選了國民黨候選人的號次,接著回到原先的台灣地理圖資,我取了每個縣市的 centroid 的座標,因為接下來畫圖的時候想標示實際得票率。這部分的操作可能比較難懂,但在後續繪圖的時候就能看出效果了。對 centroid 有興趣的人可以參考 官網的說明,簡單來說就是把各個縣市的中心抓出來,後面貼標籤的時候拿來用。

### read rds
dirpath_data <- "D:/GitHub/Dennis_R_Data_News/static/data/2020-03-15-election-results/"
df_president_county_result <- read_csv(str_c(dirpath_data, "2020/df_president_county_result_raw.csv")) %>%
dplyr::select(place, number, party, name, vote, per, vote_place)
### filter KMT presidential vote
df_president_county_result_blue <-
df_president_county_result %>%
filter(number == 2)
### add centroid column for further use
sf_county_centroid =
sf_county_geojson_simplified %>%
st_centroid(of_largest_polygon = T)
df_coordination =
sf_county_centroid %>%
st_coordinates()

接著,我用dplyr::left_join()把地理圖資和剛剛過濾得到的國民黨各縣市得票率串在一起,用來 join 的 key 是 c("COUNTYNAME" = "place"),也就是縣市的名稱。處理好資料後,就進入畫圖的階段。先看第一張圖,顏色越深代表國民黨總統候選人的得票率越高,以花東兩個縣市最高,但從圖中我們沒辦法看出實際的得票率數值,因此,我在第二張圖以 geom_text() 補上文字標籤,這些文字標籤的位置就是剛剛抓的縣市 centroid,也就是說,我把得票數字放在每個縣市的 centroid 了。此外,我也用 scale_fill_gradientn() 微調,讓顏色更貼近國民黨的官方色。到這邊基礎的統計地圖就大致完成了!

### join vote & Taiwan sf data
sf_county_president <-
sf_county_geojson_simplified %>%
left_join(df_president_county_result_blue, by = c("COUNTYNAME" = "place")) %>%
mutate(rn = row_number()) %>%
mutate(x_centroid = df_coordination[,"X"],
y_centroid = df_coordination[,"Y"])
### raw plot
sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot()+
geom_sf(size = 0.2, aes(fill = per))+
scale_fill_gradient(low = "#56B1F7", high = "#132B43")
### add text label
sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot()+
geom_sf(size = 0.2, aes(fill = per)) +
geom_text(
aes(
x=x_centroid,y=y_centroid,
label = round(per, 2)
), size=3
) +
scale_fill_gradientn(colors = c("white","#CCCCEE","#9999DD","#4141EF")) +
coord_sf(datum = NA)+
ggthemes::theme_map()

如果你覺得奇怪,怎麼看起來有點醜沒有很好看,你可以把利用 ggsave() 將圖表輸出成 SVG ,再丟到別的地方編輯,因為是向量檔,可以盡量縮放。

plot_kmt_president <-
sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot()+
geom_sf(size = 0.2, aes(fill = per)) +
geom_text(
aes(
x=x_centroid,y=y_centroid,
label = round(per, 2)
), size=3
) +
scale_fill_gradientn(colors = c("white","#CCCCEE","#9999DD","#4141EF")) +
coord_sf(datum = NA)+
ggthemes::theme_map()
### use ggsave to save plots
# ggplot2::ggsave(filename = str_c(dirpath_data, "plot_kmt_president.svg"),plot = plot_kmt_president)

Dot Distribution Map/Bubble Map

第三個要介紹的圖表類型是點示地圖,如果說統計地圖是將不同變數以特定尺度為單位化約為區域的值,那麼點示地圖就是利用點來表示變數。以剛剛的例子來說,台北市各行政區人口密度圖屬於統計地圖(每個行政區填上代表人口密度的顏色),而台北市各行政區人口分布就是點示地圖(每個行政區上散落著許多點點,每個點都代表五千人)。

底下我會呈現台灣各縣市的城鎮分佈,因為手上沒有城鎮人口的資料,所以我借用了 library(maps)world.cities 資料,裡面包含的城市的名稱、人口、經度緯度。我總共畫了兩張圖,兩張圖都是以前面仔入的台灣縣市地圖為背景地圖,再利用 geom_point() 加上城鎮分佈,第一張圖是單純呈現資料當中的城鎮分佈,第二張圖則是考慮到人口多寡,利用 size 參數調整每個點點的大小。

### get data
df_city_taiwan <- world.cities %>%
filter(country.etc=="Taiwan") %>% as_tibble()
df_city_taiwan %>% head(3)## # A tibble: 3 x 6
## name country.etc pop lat long capital
##
## 1 Changhwa Taiwan 227178 24.1 121. 0
## 2 Chaochou Taiwan 58839 22.6 121. 0
## 3 Chengkung Taiwan 18647 24.2 121. 0
### plotting: each point equals a city
sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot() +
geom_sf(size = 0.2) +
geom_point(data=df_city_taiwan, aes(x=long, y=lat)) +
theme_void()
### plotting: city population size matters
sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
ggplot() +
geom_sf(size = 0.2) +
geom_point(data=df_city_taiwan, aes(x=long, y=lat, size = pop)) +
theme_void()

Cartogram

第四個要介紹的圖表類型是示意地圖,它的長相和統計地圖類似,差別之處在於繪圖者扭曲了示意地圖當中的區域,用以代表該區域的性質,譬如底下我會扭曲台灣的各縣市,將面積依照該地區的實際投票數大小縮放。

這邊我們使用了library(cartogram)繪製示意地圖,理想上應該可以利用函數處理 sf 物件,但我的原始資料需要做一些轉換,可參考 Error with Plotting Cartogram ,使用 st_transform() 調整 CRS 後再丟到 cartogram_cont() ,就能夠跟上面一樣利用 geom_sf() 畫出好看的示意地圖了。

### create sp data
sf_county_president_carto <- sf_county_president %>%
st_crop(xmin=119,xmax=123,ymin=20,ymax=26) %>%
st_transform(crs = 3828)
### use cartogram package
library(cartogram)
president_cartogram <- cartogram_cont(sf_county_president_carto, "vote_place", itermax=5)
### plot
president_cartogram %>%
ggplot() +
geom_sf(aes(fill = per) , size=0, alpha=0.9) +
scale_fill_gradientn(colors = c("white","#CCCCEE","#9999DD","#4141EF")) +
coord_sf() +
theme_void()

Parliament Plot

第五個要介紹的圖表類型沒有專有名詞,我自己稱為議會席次圖,如果你不確定我在說什麼,可以直接參考我要使用的套件 library(ggparliament)官網,最常見的呈現形式就是將議會/立法院的各黨派人數分布以馬蹄形呈現。

這個套件的 input 簡單易懂,底下我使用了 2020 台灣立法委員的選舉結果資料,這個 dataframe 的欄位包含黨派、黨派縮寫、席次,它的原理也很簡單,以馬蹄形的議會圖為例,套件當中的 parliament_data() 函數會利用提供的黨派與席次,計算出繪製馬蹄形所需的點座標與角度。使用glimpse()看處理後的資料,就可以發現新的 df_parliament_final 已經從原本 aggregated data 推回 individual data 了。

我選擇半圓形(semicircle)作為繪製的席次分佈形狀,除此之外還有馬蹄形、圓形等多元的選擇,也有幫忙你計算各政黨席次占比的橫條圖、強調過半政黨的函數,作者實在用心良苦,幸好沒有錯放你的手。

library(ggparliament)
### get data
df_parliament_summary_raw <- read_rds(str_c(dirpath_data, "2020/df_parliament_summary_raw.rds"))
# df_parliament_summary_raw <- read_csv(str_c(dirpath_data, "2020/df_parliament_summary_raw.csv"))
### take a look
df_parliament_summary_raw %>% head(5)
## year country house party_long
## 1 2020 Taiwan Legislative Yuan Democratic Progressive Party
## 2 2020 Taiwan Legislative Yuan Kuomintaing
## 3 2020 Taiwan Legislative Yuan Taiwan Statebuilding Party
## 4 2020 Taiwan Legislative Yuan New Power Party
## 5 2020 Taiwan Legislative Yuan Taiwan People Party
## seats government colour
## 1 61 1 #1b9431
## 2 38 0 #000095
## 3 1 0 #a73f24
## 4 3 0 #fbbe01
## 5 5 0 #28c8c8
### use ggparliament::parliament_data to create cooridinate system & theta
df_parliament_final <- parliament_data(election_data = df_parliament_summary_raw,
type = "semicircle",
parl_rows = 6,
party_seats = df_parliament_summary_raw$seats)
df_parliament_final %>% head(5) %>%
dplyr::select(party_short,seats,government,colour,x,y,row,theta)
## party_short seats government colour x y row theta
## 1 DPP 61 1 #1b9431 -2.0 2.449213e-16 6 3.141593
## 1.1 DPP 61 1 #1b9431 -1.8 2.204291e-16 5 3.141593
## 1.2 DPP 61 1 #1b9431 -1.6 1.959370e-16 4 3.141593
## 1.3 DPP 61 1 #1b9431 -1.4 1.714449e-16 3 3.141593
## 1.4 DPP 61 1 #1b9431 -1.2 1.469528e-16 2 3.141593
### plot: simple
df_parliament_final %>%
ggplot(aes(x, y, colour = party_short)) +
geom_parliament_seats(size = 5)
### plot: complete
df_parliament_final %>%
ggplot(aes(x, y, colour = party_short)) +
geom_parliament_seats(size = 5) +
geom_highlight_government(government == 1) +
geom_parliament_bar(colour = colour, party = party_long) +
draw_majoritythreshold(n = 57, label = TRUE, type = 'semicircle')+
theme_ggparliament() +
#other aesthetics
labs(colour = NULL,
title = "Taiwan Legislative Yuan",
subtitle = "Party that controls the Legislative Yuan highlighted.") +
scale_colour_manual(values = unique(df_parliament_final$colour),
limits = unique(df_parliament_final$party_short))
#其他可以選的類型
#type = "horseshoe", "semicircle", "circle", "classroom", "opposing_benches"

Hexmap/Tilegram

第六個要介紹的圖表類型是六角型地圖(hexmap),或是稱為 tiled cartogram,是示意地圖的延伸。顧名思義,tiled cartogram 由許多 tiles(瓦片)組成,每個 tiles 都代表一個均等的區域,可以是人口數上的均等,也可以是其他特定變數,譬如美國選舉人團的均等。

使用 hexmap 的圖表好處在於,不會因為某些地理特性而誤解區域的影響力,以選區來說,雖然台北市的面積小於花蓮,但台北的區域立委席次較多,若直接用區域面積繪製立委得票,無法呈現台北市立委的影響力,相對而言使用以選區為單位的 hexmap,可以直接看到各縣市在選舉中產生多少位立委。

若你覺得聽起來抽象,不妨參考一個用 JavaScript 寫的 tiled maps 生成器,底下我會簡單說明繪製這種圖表的方法,並以台灣 2020 分區立委選舉的選區實作。

目前我看到繪製 tilegrams 的方法主要有兩種,第一種是直接繪製出特定格式的 JSON,第二種是用數學方法將地理資料切分出多個 tiles。就前者而言,tilegramsR 套件上就有整理前人的多樣作品,而 R Graph Gallery 上也有教學,它會請你先下載某個 GeoJSON,載入 R 裡面就已經是 hexmap 的形式了,但遺憾的是這上面都沒有台灣的資料,所以我等一下會用其他方法畫一遍。就後者而言,你可以參考 Stackoverflow 上的 how to draw tilegram/hexagon map in R,有位熱心人士提供自己寫的測試套件,我有測了一下確定可行,只是把台灣切成數的 tiles 的效果沒有那麼好而已。

底下我使用的套件是 library(clhex),這個套件可以產生 hexJSON,以台灣為例,我先產生一個包含 2020 區域立委選舉選區的 dataframe,接著利用 create_hexjson()產生 hexJSON,再將它丟到 hexjson editor 編輯。

### create hexJSON based on constituency
library(clhex)
df_legis_constituency <- read_rds(str_c(dirpath_data, "2020/df_legis_constituency.rds"))
hex_test <- create_hexjson(df_legis_constituency)
### 我已經create過了
# create_and_save_hexjson(df_legis_constituency, str_c(dirpath_data, "2020/output.hexjson"))

在 hexjson editor 上的編輯過程是純手動拖拉,請參考下圖。選擇上傳後有問你一些設定的問題,可以參考我的作法填寫,編輯模式開啟後可以將選區依照地理位置移動,移動方法是選取 hex 然後點選要更換位置的另一個 hex,各點擊一次就可以交換位置了,我用了一個奇怪的形狀當範例,畫好後就可以選擇下載 GeoJSON 了。

上傳 hexJSON 的時候網站會問你要怎麼設定
上傳後長這樣
拖拉每個選區後的長相,假設台灣長這樣,接下來就可以下載了

按照上面的步驟,將檔案載入 R 後丟到 geom_sf()裡面就可以畫圖囉!

### download data from https://olihawkins.com/project/hexjson-editor/
### import data
json_2020 <- geojson_read(str_c(dirpath_data, "2020/hexmap.geojson"))
sf_2020 <- json_2020 %>% as.json() %>% geojson_sf()
### plot
sf_2020 %>%
ggplot() +
geom_sf(size = 0, aes(fill = value)) +
scale_fill_manual(values=c("#4141EF", "#33AE33", "#808080")) +
coord_sf(datum = NA) +
ggthemes::theme_map() +
theme(legend.position = "null")

你可能會想問兩個問題,第一個是為什麼要那麼麻煩產出 hexJSON ,不乾脆用 Illustrator 拉就好,第二個是為什麼 ggplot2 的結果有點歪。第二個問題我自己沒有解答,目前作法也是丟到 Illustrator 調整比例;針對第一題,我的回答跟前面的一樣,如果只是一次性的任務那用畫的很快,但如果你要畫台灣歷次分區立委選舉的結果,倘若你有選區名稱跟結果,就可以直接跟這個 hexJSON 串不用自己慢慢填色。

Conclusion

Some thoughts and recommended books

每次看到紐約時報、華盛頓郵報、彭博社在美國選舉期間多樣的數據圖表,或簡潔或繁複,都會驚嘆於不同敘事方式背後編輯們的深刻設想。2018年台灣五都與地方縣市首長選舉時,天下雜誌、關鍵評論網、中央社等媒體也都在視覺化呈現選舉結果上下了很多苦工,參加 HH Taipei 的分享後,我在心裡想,自己也要做一樣的事情,2020 選舉時因緣際會畫了一些選舉圖表,在事前規劃的過程中累積了上面的探索跟程式碼,也因此產出了這篇文章。

除了長條圖、散點圖以外,很多類型的選舉圖表始終離不開地圖,因此如果你想要深入這個領域,可以參考這兩本書,第一本是 Geocomputation with R,教你怎麼靈活的操作地理資料,並且有跨領域的應用,個人非常推薦這本書;第二本則是臺北大學林茂廷老師撰寫的授課用書,因為是用中文寫的所以比較親民。

A short review

在這篇文章當中,我利用 R 語言繪製了六種類型的選舉圖表,包含 Background Map、Choropleth、Dot Distribution Map、Cartogram、Parliament Plots、Hexmap/Tilegram,希望你會喜歡!若你看到有趣的圖表,也歡迎和我說。特別感謝照真老師的課堂,還有 Tzu-Lun 跟我一起畫圖表,也謝謝 Summit 邀請我分享才催生這篇文。

--

--

Dennis Tseng
Dennis, R, data, news

現在不是新聞所學生也不算資料分析師了,變成記者。對商業分析、統計、資料視覺化、資料新聞都很有興趣,喜歡寫 R!