R 技術文 — 互動式 Chord Diagram

Eric
14 min readApr 20, 2019

--

前文提及以 R 製作 Heat Map,再接再厲(實情是票房毒藥,當是給自己看的筆記好了,由於用上台灣的數據,用回唔鹹唔淡的書面語好了 XD),談談以 R 製作互動式 Chord Diagram。Chord Diagram 適宜表達不同單元之間的(流量)關係,例如地鐵站與站之間的乘客量、國與國之間的人員流動往來、甚或個體如何在不同分類轉換狀態,先看最後成果:

台北捷運分時各站 Origin-Destination 流量統計 (17.02.2019, Hr=18)

1. 數據來源

此文原始數據集 (臺北捷運每日分時各站OD流量統計) 源自臺北市政府資料開放平台,內有北捷每日分時每對起訖點的乘客量,以 2019年2月的數據為例,別小看下載檔案僅 12 MB 左右,解壓後可是個超過 600MB 的檔案,內有接近七百萬行 (rows,據說兩岸用法不同,先說清楚免生歧義)。當然可以嘗試用 Excel 開啟檔案,不過只能顯示首一百萬行左右的資料。

雖然檔案是以 CSV 為副檔名,但打開後就會發現跟想像有點出入

說好的逗號呢?

找不到逗號,那如何拆解為所需欄位呢?第二行的那堆 “-” 是提示,實際格式為固定寬度文字檔,亦即不同欄位寬度不變,只消數數各欄位多寬即能分割為五個欄位。

這次會用到數個 packages,主菜當然是 “chorddiag”,前期設定如下,在此不贅。

## INSTALL (IF NECESSARY) AND LOAD THE PACKAGES 
if("devtools" %in% rownames(installed.packages()) == FALSE) {
install.packages("devtools", repos="http://cloud.r-project.org/")
}
library("devtools")
if("chorddiag" %in% rownames(installed.packages()) == FALSE) {
devtools::install_github("mattflor/chorddiag", force=TRUE)
}
if("tidyverse" %in% rownames(installed.packages()) == FALSE) {
install.packages("tidyverse", repos="http://lib.stat.cmu.edu/R/CRAN")
}
## LOAD THE PACKAGES
library("chorddiag")
library("tidyverse")
## SET THE WORKING DIRECTORY
setwd("D:/Blog/Medium/Chorddiag")
## SET THE LOCALE TO CHINESE
Sys.setlocale(locale = "Chinese")

2. 讀取數據

readr (載入 tidyverse 時會自動順手載入) 的 read_fwf() 可以快速讀取這類 Fixed Width File,由於習慣不以中文來命名變數,直接跳過首兩行 (skip=2),再列出各欄位的寬度(如日期的寬度為 11)即可。

## LOAD THE RAW FILE
df2 = read_fwf("OD_201902.csv", skip=2, fwf_widths(c(11, 7, 31, 31, NA),c("Date", "Time", "Ori", "Dest","Cnt")))

隨後會有警告訊息提示檔案有些小問題,皆因最後四行只是些註解,不屬於行程數據,那就直接移除這數行好了。檢查 data frame 內的變數類型,Time 為 Chr (i.e. character),那就順手將此轉換為整數,放在新欄位 Hour。(預先為後續文章轉換)

需要移除最後四行
## INSPECT THE LAST 10 ROWS OF THE DATA
tail(df2, 10)
## REMOVE THE LAST 4 ROWS OF DATA
df2 = df2[1:(nrow(df2)-4),]
## CONVERT THE TIME (in Chr) to INTEGER
df2$Hour = as.integer(df2$Time)

車站眾多,最直接就是以路線來分門別類,轉車站則以啟用日期先後決定撥往哪條線,之後再按路線指定顏色。雖亦曾以車站所在區域來分類,不過太多車站橫跨區域邊界,亦實在不知撥向哪區是好,只好作罷。

## LOAD THE COLOR CODE FILE
df_color = read_csv("Stn3.csv")

3. 選取數據

假如你只想專注於某日某小時而非整個二月的數據,那就要以 filter 從那六百多萬抽取符合篩選條件的數據,步驟簡單易明,只須列明條件,如Date=="2019-02-17",多於一個條件就加 “&” (意即 AND) 、 “|”,(意即 OR)。

## APPLY FILTER (SPECIFIC DATE AND HOUR)
fil_data <- df2 %>%
filter(Date == "2019-02-17" & Hour == 18)
## EXTRACT RELEVANT COLUMNS
plot_data <- as.data.frame(fil_data[,3:5])
## 這是 R,不是 Python (x 10000),column index 由 1 開始數

然後就要將篩選後的資料配上車站顏色,這裏會用到 left_join,如果對數據庫操作有基本認識的話應該不會陌生,不太熟悉的可以看這裏 (Python pandas documentation),而在這例子由於一個車站只會對應一種顏色,可簡單看成 Excel 的 vlookup。原始檔並非完全按近年新增的車站編號順序列出,如第一行的松山機場站為(BR13),續遞減至動物園站 (BR01),再跳至大直站 (BR14),繼而遞增排列,為免同綫相鄰車站在 Chord Diagram 上相隔甚遠,故以車站編號 (先以起始站,再以目的地) 先行排序一次,方作後續處理。

## MAP WITH STATION CODE
plot_data <- left_join(plot_data, df_color[c(1,3)], by=c("Ori"="Station"))
plot_data <- left_join(plot_data, df_color[c(1,3)], by=c("Dest"="Station"))
## RE-ORDER USING STATION CODE
plot_data <- plot_data[order(plot_data[,4], plot_data[,5]), ]

4. 建立 Origin-Destination Matrix

下一步是建立個大小為 108 × 108 的Origin-Destination 矩陣 (數據集涵蓋 108 個車站,故此共有 108 × 108 行程組合,包含同站進出),每格代表該時段由站 A 前往站 B 的人次。以 spread 將 Long Form 轉換為 Wide Form 或會掉失數據的次序,故此在轉換前做了些額外功夫保留數據次序,之後補回每一行的名稱後才轉為矩陣。最後要另外儲存每個車站的顏色,而次序須與 矩陣的 rows 的次序相同(下面段 code 偷了點懶 :p)

## CREATE FACTOR LEVELS FOR AND PRESERVE THE ORDER
plot_data$Ori = factor(plot_data$Ori,(as.character(unique(plot_data$Ori))))
plot_data$Dest = factor(plot_data$Dest,(as.character(unique(plot_data$Dest))))
## CONVERT FROM LONG FORM TO WIDE FORM
plot_data_mat <- spread(plot_data[1:3], "Dest", "Cnt")
## ASSIGN THE ROWNAMES AND CONVERT THE DATA TO A MATRIX
rownames(plot_data_mat)=plot_data_mat$Ori
plot_data_mat <- as.matrix(plot_data_mat[,-1])
## A LIST OF COLORS FOR DIFFERENT STATIONS IN THE SAME ORDER
df_color <- df_color[order(df_color$Code), ]
groupColors <- df_color$Color

5. 以 chorddiag 製作互動式 Chord Diagram

只需一「行」代碼就完成製作,並可按個人喜好調整顯示參數。在 RStudio 按 Zoom 就會彈出互動式視窗,由於有過萬個組合,眼花撩亂吧?將鼠標移至車站之上,就會只 highlight 與之有關的行程組合,而放在特定線段之上,就會顯示雙向人次,最後更可以匯出至網頁,再上載至網上與人分享,十分方便。

## GENERATE AN INTERACTIVE CHORD DIAGRAM
chorddiag(plot_data_mat, groupColors=groupColors, groupnamePadding=20, groupnameFontsize=15, showTicks=F, chordedgeColor=NULL, groupThickness=0.2)
把鼠標放在台北車站的紅色邊
把鼠標放在 台北車站 <> 西門 的線段之上
Save as Web Page,匯出至網頁,一鍵即可

完整原始碼:

## INSTALL (IF NECESSARY) AND LOAD THE PACKAGES 
if("devtools" %in% rownames(installed.packages()) == FALSE) {
install.packages("devtools", repos="http://cloud.r-project.org/")
}
library("devtools")
if("chorddiag" %in% rownames(installed.packages()) == FALSE) {
devtools::install_github("mattflor/chorddiag", force=TRUE)
}
if("tidyverse" %in% rownames(installed.packages()) == FALSE) {
install.packages("tidyverse", repos="http://lib.stat.cmu.edu/R/CRAN")
}
## LOAD THE PACKAGES
library("chorddiag")
library("tidyverse")
## SET THE WORKING DIRECTORY
setwd("D:/Blog/Medium/Chorddiag")
## SET THE LOCALE TO CHINESE
Sys.setlocale(locale = "Chinese")
## LOAD THE RAW FILE
df2 = read_fwf("OD_201902.csv", skip=2, fwf_widths(c(11, 7, 31, 31, NA),c("Date", "Time", "Ori", "Dest","Cnt")))
## INSPECT THE LAST 10 ROWS OF THE DATA
tail(df2, 10)
## REMOVE THE LAST 4 ROWS OF DATA
df2 = df2[1:(nrow(df2)-4),]
## CONVERT THE TIME (in Chr) to INTEGER
df2$Hour = as.integer(df2$Time)
## LOAD THE COLOR CODE FILE
df_color = read_csv("Stn3.csv")
## APPLY FILTER (SPECIFIC DATE AND HOUR)
fil_data <- df2 %>%
filter(Date == "2019-02-17" & Hour == 18)
## EXTRACT RELEVANT COLUMNS
plot_data <- as.data.frame(fil_data[,3:5])
## MAP WITH STATION CODE
plot_data <- left_join(plot_data, df_color[c(1,3)], by=c("Ori"="Station"))
plot_data <- left_join(plot_data, df_color[c(1,3)], by=c("Dest"="Station"))
## RE-ORDER USING STATION CODE
plot_data <- plot_data[order(plot_data[,4], plot_data[,5]), ]
## CREATE FACTOR LEVELS FOR AND PRESERVE THE ORDER
plot_data$Ori = factor(plot_data$Ori,(as.character(unique(plot_data$Ori))))
plot_data$Dest = factor(plot_data$Dest,(as.character(unique(plot_data$Dest))))
## CONVERT FROM LONG FORM TO WIDE FORM
plot_data_mat <- spread(plot_data[1:3], "Dest", "Cnt")
## ASSIGN THE ROWNAMES AND CONVERT THE DATA TO A MATRIX
rownames(plot_data_mat)=plot_data_mat$Ori
plot_data_mat <- as.matrix(plot_data_mat[,-1])
## A LIST OF COLORS FOR DIFFERENT STATIONS IN THE SAME ORDER
df_color <- df_color[order(df_color$Code), ]
groupColors <- df_color$Color
## GENERATE AN INTERACTIVE CHORD DIAGRAM
chorddiag(plot_data_mat, groupColors=groupColors, groupnamePadding=20, groupnameFontsize=15, showTicks=F, chordedgeColor=NULL, groupThickness=0.2)

Disclaimer:

資料提供機關: 臺北大眾捷運股份有限公司
[2019年] [ 捷運每日分時各站OD流量統計]

此開放資料依政府資料開放授權條款 (Open Government Data License) 進行公眾釋出,使用者於遵守本條款各項規定之前提下,得利用之。

政府資料開放授權條款:
http://data.gov.tw/license

--

--

Eric
Eric

Responses (1)