1
R 軟體套件介紹 : data.table
王博賢 副統計分析師 在現在這個資料龐大且雜亂的時代,R 本身的函數也漸漸的越來越跟不上這 個時代,一些基本的函數使用起來變得很沒有效率,甚至在資料量大的時候根本無 法使用,因此漸漸的有人發展了其他的package 來幫助我們進行資料清理,像是常 見 的 “dplyr” 。 而 近幾 年 有 另 外 一 個 package 崛 起 , 那 就 是 今天 要 介 紹的 data.table。 首先要使用時,記得要先下載 package,下載後記得要呼叫他。語法如下 install.packages("data.table") library(data.table) 讀取及輸出資料 第一個要跟大家介紹的功能是輸出資料,在傳統的 R 中寫出資料常用的語法 是使用“write.csv”,但在輸出的資料很大時,write.csv 會需要很長的時 間,甚至可能就當機了。而 data.table 提供了更快速的方法,他的語法為 “fwrite”,下面我們來測試兩種方法輸出資料時所需的時間。程式碼如下 : #生成 5000*5000 的資料 n <- 5000 dt.test <- data.table(matrix(rep(1,(n*n)),nrow=n)) #fwrite ptm <- proc.time() fwrite(dt.test,"test.csv") dt.time <- proc.time() - ptm #write.csv ptm <- proc.time() write.csv(dt.test,"test.csv") base.time <- proc.time() - ptm臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12
2
cat("fwrite use time :",dt.time[1], "write.csv use time :",base.time[1])
而得到的結果如下圖: 我們可以從上面的結果來看,在輸出 5000*5000 (大約 47.7 MB)的資料時, fwrite 幾乎是瞬間就完成了,而 write.csv 則花了 19.83 秒以上,可想而知在 現在資料動不動就是好幾 GB 甚至 TB ,如果我們使用 write.csv 那可能輸出資 料就要等上好幾個小時以上。 接著要跟大家介紹的是讀取資料,在一般常見的語法為“read.csv”而在 data.table 中提供另一種讀取資料的語法為“fread”,下面就讓我們來看兩著 的效率,程式碼如下 : ptm <- proc.time() dt1 <- fread("test.csv") dt.time <- proc.time() - ptm #write.csv ptm <- proc.time() dt2 <- read.csv("test.csv") base.time <- proc.time() - ptm cat("fread use time :",dt.time[1], "read.csv use time :",base.time[1])
3 結果如下 : 我們試著把剛寫出的資料讀進來,我們可以看到在 47.7 MB 的資料時,一樣的 fread 的效率遠遠的超越了 write.csv ,在資料量大時,他們的差異只會越來 越大,因此建議大家在讀取大型資料時,使用 fread 他可以大幅度的減少您所需 等待時間。 在利用 R 處理大型的資料時,最常遇到的問題是記憶體不足,導致根本無 法讀取資料,而在這個問題上 fread 也有提供一些特別的功能,那就是他提供 我們可以控制我們要 a. 跳過幾筆資料,b. 一次要讀取幾筆的資料,有這兩種功 能後,我們就可以分段處理資料,先讓資料量變小,以方便我們進行後續的分 析。語法如下 data1 <- fread("http://biostat.tmu.edu.tw/attachment/download.php?atta chment_id=94") data1 #分段讀取 data2 <- fread("http://biostat.tmu.edu.tw/attachment/download.php?atta chment_id=94",nrows = 10 ,skip = 64000) data2 結果如下 :
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 4 我們可以從 data1 的結果看到資料共有 64489 筆,而當我控制 nrows = 10 (讀進來的筆數),skip = 6000 (略過的筆數) ,我們發現資料指讀取 64000 到 64009 共只有 10 筆資料,可能您會覺得很奇怪為什麼跳過 64000 ,他是從 64000 筆讀取,那是因為第一列的欄位名稱也算是一列,且您可以發現 data2 的部分並沒有欄位名稱,因此在使用上要注意,因為跳過了首列,所以並不會知 道資料的欄位名稱,所以要在自己補上去。 data.table 的基本介紹 接著是 data.table 的基本介紹 ,下面我們使用的範例資料為 CVD_all 網址為為 http://biostat.tmu.edu.tw/attachment/download.php?attachment_id=94
5 語法如下: data1 <- fread("http://biostat.tmu.edu.tw/attachment/download.php?atta chment_id=94") data2 <- read.csv("http://biostat.tmu.edu.tw/attachment/download.php?a ttachment_id=94") data1 data2 結果如下 : 我們可以看到,data.table 在顯示資料方面做得很好,我們不需要預設我 要看前幾列,他自然會幫我們顯示前五筆及最後五筆,而有用過 data.frame 的 人,應該都知道如果沒有控制顯示的筆數,他會顯示一長串,這樣很難幫助我們 快速的看一下資料。這就是 data.table 的一個好處。 在我們利用 fread 讀取資料時,讀進來的資料自然就為 data.table 的形 式,而如果您利用其他方法讀去資料,或是其他的資料型態的資料,要轉換為 data.table 時,那您只需使用 data.table() 即可,語法如下 : str(data2) data2.dt <- data.table(data2) str(data2.dt) 結果如下 :
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 6 我們可以看到資料的型態從 data.frame 轉換成了 data.table。 接著就要準備進入我們的正題了,下面為 data.table 整體的概念 : DT[i, j, by] i j by
where | order by select | update group by 第一個位置 i : 主要是用來下篩選資料的條件,或是排序用。 第二個位置 j : 主要是用來篩選欄位,或是更新欄位。 第三個位置 by : 依照 group 去做事情。 data.table 資料選取及篩選 首先跟大家介紹如何篩選資料,語法如下: #data.table data1[心血管疾病 == 0 & 年齡 > 50] #data.frame
data2[data2$心血管疾病 == 0 & data2$年齡 > 50,]
7 我們可以看到,使用 data.table 篩選資料時,我們不必像一般 data.frame 一直使用 “data2$” ,我們只需要直接寫欄位名稱,且這次我們再一次看到 data.table 的好處,經過篩選後,我們可以很快用肉眼看出滿足“心血管疾病 是 0 且 年齡 >50”的有 19034 筆,假設您使用的是 data.frame,他只會把所 有的資料印出來,最後面再加個 …. ,沒有辦法直接讓我們看出資料的筆數。 接著介紹欄位的篩選,假設今天我們要選擇 “心血管疾病”,語法如下 #data.table data1[1:10,心血管疾病] #data.frame data2[1:10,"心血管疾病"] data1[,list(心血管疾病)] data1[,.(心血管疾病)] #selct column note ---- a <- "心血管疾病"
data1[,a,with=F]
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 8 從最上面結果我們可以看到,在 data.table 中選擇欄位一樣直接輸入欄位 名稱即可,並不用像 data.frame 一樣再加上 “ ” ,而在 data.table 中您可 以使用 “list()” 或是 “.()” 來保持 data.table 的樣式而非直接是 vector。 而在某些狀況時,例如寫 for 迴圈時,我們可能是使用變數代替變數名 稱,假設 a <- “心血管疾病” ,我們要用時要在後面加上 “with = F” 這 個指令可以幫助我們轉換回 data.frame 的語法,這樣我們就可以讀取資料,但 要注意的是,用這種方法得到的結果,只能是 data.table ,假設您要的是 vector 則要在後面加上[[1]]就可以了。主要是因為他本身也是 list 的資料型 態,因此我們可以使用此方法得到 vector。 而假設今天我要選取多個欄位,語法如下
9 #data frame dt2 <- data2[,c("ID","年齡","收縮壓","舒張壓")] #data table dt1 <- data1[,.(ID,年齡,收縮壓,舒張壓)] dt1 結果如下 : data.table 可以直接用 .() ,加上欄位名稱即可,不需要再加上 c() 以及 “ ”。 而 data.table 另一個好處為我們可以同時篩選資料,並且更改欄位名稱, 假設我今天要篩選出前 10 筆,並只要 ID 及 年齡,且年齡要改名為 age 時, 我只需要短短一句即可 ,語法如下 :
#select and rename
dt1[1:10,.(ID,age=年齡)]
結果如下 :
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 10 “>=50”,不是的為 “<50”,那語法如下 : #data.frame age.group <- ifelse(dt2$年齡 >= 50,">=50","<50") dt2 <- cbind(dt2,age.group) dt2[1:5,] #data.tab;e dt1[,age.group:=ifelse(dt2$年齡 >= 50,">=50","<50")] dt1 結果如下 : 從上面看到,如果您是使用原本的 data.frame,你可能就需要用到 3 個步 驟才能完成,但如果使用 data.table 則可以用一個步驟就完成他。在 data.table 中新增欄位我們只需要使用 “:=” 即可在最後面新增一個變數。 data.table 的計算 data.table 在計算及聚合上,也提供了快速且方便的方法,假設今天我要計 算收縮壓的平均值,語法如下: dt1[,mean(收縮壓,na.rm=T)] 結果如下 : 假設今天我們要的是同時計算平均數及標準差,那一樣我們只需用一行指令
11 即可,語法如下 : dt1[,.(mean(收縮壓,na.rm=T), sd(收縮壓,na.rm=T))] 結果為 : 因為在 data.table 中,假設您沒有為欄位命名,他認定會使 V1,V2…來為 欄位命名,假設今天你想在計算的同時給定欄位名稱的話,語法如下 : dt1[,.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T))] 結果為 : 從上面我們可以看到,我們只需要加上 “欄位名稱 = function(x)”,就 可以快速的得到聚合好的資訊,且名稱也為我們所需要的。 接著一樣的我們在計算的同時,我們也可以針對資料進行篩選。假設今天我 們只想知道 age.group 為 “>=50” 這群人的收縮壓之平均值及標準差,語法 如下
dt1[age.group == ">=50",.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N)] 結果如下 : 從上面結果來看我們只需要在 i 的位置增加篩選條件即可。因此我們只需 短短一行指令,就可以同時完成資料的篩選及計算,大幅度簡短了所需撰寫的程 式碼及閱讀性。 接著跟大家介紹 “by” 的用法,回到上面那個例子,我們只針對
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 12 age.group 為 “>=50” 的進行計算,但通常我們會想同時知道的是 ”>=50” 及 “<50” 兩群的資訊,而這時我們就可以使用 by 來幫助我們計算,語法如 下 : dt1[,.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N),by=age.group] dt1[,.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N),by=.(年齡>=50)] 結果如下 : 從上面我們可以看到只需要在原本的程式碼後面加上 “by = 分組變數 “ 即可,或是也可以用“by =邏輯判斷”,data.table 就可以幫我們進行計 算。 一樣的在進行分組計算時,我們可以先對資料進行篩選。假設現在我們不想 看到 NA 這群的結果,那我之需要在前面 i 的位置先排除掉 age.group 為 NA 的資料即可,語法如下 : dt1[!is.na(age.group),.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N),by=age.group] 結果為 : 當然在很多情況下,我們的分組變數不會只有一個,加設今天我要再加上性
13 別做為分組變數,語法如下 dt1[,gender:=data1$性別] dt1[!is.na(age.group),.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N),by = .(age.group,gender)] 結果如下 : 因為我們剛在選擇欄位時,並沒有選擇到性別,因此我們先利用前面提到的 “:=” 為 dt1 新增一欄為 gender,裡面的內容為原本檔案中的性別。接著我們 只需要在 by 的地方在新增上 gender 即可。 接著聚合完後,我們有時還會對資料進行篩選或排序,在 data.table 中我 們可以使用 [ ] 接著對輸出的結果做動作。 假設今天聚合完後,我要篩選出 mean 小於 130 後再對 mean 做遞減的排 序,那語法如下 : dt1[!is.na(age.group),.(mean = mean(收縮壓,na.rm=T), sd = sd(收縮壓,na.rm=T), N = .N),by = .(age.group,gender)][mean < 130] [order(-mean)] 結果如下 : 接著最後跟大家介紹“.SD”,在 data.table 中 “.SD” 代表著整個資 料,以下面例子為大家介紹,語法如下: dt2 <- data1[,.(收縮壓,舒張壓,空腹血糖,高密度脂蛋白,三酸甘油酯)] dt2 dt2[,lapply(.SD,mean,na.rm=T)] 結果如下:
臺北醫學大學生物統計研究中心 eNews 第 34 期 2019/12 14 首先我們先從原先的資料中,選擇 收縮壓、舒張壓、空腹血糖、高密度脂 蛋白、三酸甘油酯 5 個數值變數。接著我們想要知道這五個變數各自的平均值, 此時我們只需要在 j 的位置中,使用 lapply(.SD,mean,na.rm=T) 即可,其中 的 .SD 就表示 dt2 整個資料集。如果對 lapply 不了解的,可以使用 ?lapply 語法來查看他。 接著帶大家看“.SD”實際應用的例子,假設今天我要對 dt1 中的收縮壓 及 舒張壓 做標準化的動作,並且是在原本的資料集中進行新增,語法如下 : dt1[, c("SBP", "DBP") := lapply(.SD,scale),.SDcols = c("收縮壓","舒張 壓")] dt1 結果如下: 一樣我們可以使用 “:=” 一次新增兩個以上的欄位,只是前面就要以傳統 vector 的形式來撰寫,接著一樣使用 lapply(.SD,scale) 來做計算,但這次我 們並非要對整個 dt1 做計算,因此我們可以在後面加上 “.SDcols = “ 來選擇 我們所需的欄位即可。 今天跟大家簡單介紹了 data.table 這個 package 的一些內容,提供大家在
15 資料處理上的另一種方法。他不只快速,且簡潔易讀,且這個 package 還有很多 不錯的 function,如果大家有興趣,可以仔細去讀他的說明檔,相信會有很多的 收穫。 參考資料 : 1. https://cran.r-project.org/web/packages/data.table/vignettes/datatable-intro.html 2. https://cran.r-project.org/web/packages/data.table/data.table.pdf