最近瀏覽SO上R語言標籤下投票最多的問題,想著一一整理成中文大家一起分享,幸運的是正好發現一篇試圖使用data.table包解決票數最多的50個問題的文章,看了這些答案不禁讓人感慨萬千。
其實前50的問題中不少可以使用data.table包高效解決,比一般的解決辦法快很多倍,特別是面對超大數據時(我實在不願意使用大數據這個詞,感覺它已經被人濫用到無以復加的程度),如果需要的話我會測試給大家看。是的,從接觸R,我們就已經開始熟悉各種問題的解決辦法,並把它們記在心裡。SO上很多問題早就有了高票答案,當時data.table包還沒有開發,但是現在知道這些高效的答案並不是壞事。有時候作為技術人員要時刻準備更新自己的觀念、知識和技能,這是技術猿的宿命。如果一個技術猿失去了開放的大腦,很容易就從他的身上嗅出大叔的味道,因為他已經老了,更嚴重的是在職業發展的道路上要不被技術淘汰,要不轉做管理人員。是的,一夜之間你以前所熟悉的答案,已經成了現在的笑話了。
下面是前50的高票問題,及其與data.table的關係。
對一些高票答案進行對比一下,就可以看出data.table的優勢。
根據變量(列)z和b排序,z按照降序,b按照升序。首先創造一個足夠大的數據集,然後分別使用不同的方法看一下效果。
數據集require(plyr)require(data.table)require(dplyr)require(taRifx)if (!suppressWarnings(require("doBy"))) { install.packages("doBy") require("doBy")}if (!suppressWarnings(require("taRifx"))) { install.packages("taRifx") require("taRifx")}if (!suppressWarnings(require("dplyr"))) { install.packages("dplyr") require("dplyr")}set.seed(45L)dat = data.frame(b = as.factor(sample(c("Hi", "Med", "Low"), 5e7, TRUE)), x = sample(c("A", "D", "C"), 5e7, TRUE), y = sample(100, 5e7, TRUE), z = sample(5, 5e7, TRUE), stringsAsFactors = FALSE)
對一億條數據進行多變量排序,下面是不同解決方法耗費的時長和內存佔用量。可以將下面的解決方法包括在system.time()函數中執行查看他們的耗時,
orderBy( ~ -z + b, data = dat) ## 來自doBy包plyr::arrange(dat, desc(z), b) ## 來自plyr包arrange(dat, desc(z), b) ## 來自dplyr包sort(dat, f = ~ -z + b) ## 來自taRifx包dat[with(dat, order(-z, b)), ] ## 來自R基礎包# 將數據框轉化為data.table,並建立索引setDT(dat)system.time(dat[order(-z, b)]) ## data.table對象, 使用R基礎函數system.time(setorder(dat, -z, b)) ## data.table對象, 使用setorder()函數 ## setorder() 現在已經可以對數據框對象使用 # R程序內存佔用量 = ~2GB ('dat'佔用內存)# # Package function Time (s) Peak memory Memory used# # doBy orderBy 409.7 6.7 GB 4.7 GB# taRifx sort 400.8 6.7 GB 4.7 GB# plyr arrange 318.8 5.6 GB 3.6 GB # base R order 299.0 5.6 GB 3.6 GB# dplyr arrange 62.7 4.2 GB 2.2 GB# # data.table order 6.2 4.2 GB 2.2 GB# data.table setorder 4.5 2.4 GB 0.4 GB#
1.data.table的DT[order(…)]語句是眾多方法中最快的,幾乎是dplyr包10倍以上的速度,佔用的內存和dplyr包相同。
2.data.table的setorder()函數是dplyr速度的14倍,而高峰內存佔有量僅僅增加了0.4G。
data.table包特點:
速度data.table的排序速度非常快,因為它使用了基數樹(radix ordering)。DT[order(…)]雖然是R原有的語法,但是已經經過了data.table對象方法的優化,速度非常快,內存佔用也比較少。
內存一般情況下對數據排序後就不再需要原來的表格,即將排序的結果賦值給原對象,例如:
DF <- DF[order(...)]
在這個過程中有一段時間將要佔用原有對象兩倍的內存,為了優化內存的管理,data.table包提供了setorder()函數,該函數對data.table對象排序使用了索引,而不用拷貝原有的數據,僅僅多佔用原有對象一列數據所佔用的內存。以上來自Arun的回答。
R Grouping functions: sapply vs. lapply vs. apply. vs. tapply vs. by vs. aggregate(比較apply家族、by及aggregate透視表函數)很多人對apply家族函數的區別比較困惑,其實他們是R用來快速處理循環過程的函數,只是他們的輸入和產出略有不同,也就造成了他們名字的首字母不同具體,可以參看下圖:
但是我們今天從另外一個角度比較他們,即他們的效率問題。其實不同的函數所要求的輸入和輸出對象不同,但是本質上他們都是對某一對象的分組和分步計算,apply函數要求的輸入與其他函數差異較大造成調整時間比較大,這裡就不把apply函數列入討論範圍了。
我們對500萬個數據分5萬組求和及計數,就是sum和length函數,針對兩個最近比較流行但並沒有廣泛使用的包data.table和dplyr進行測試,分別測試並將每一種方法整體所需時間存儲在timing裡面。無論結果如何這兩個包都應該馬上加入你的工具箱。
if (!suppressWarnings(require("dplyr"))) { install.packages("dplyr") require("dplyr")}if (!suppressWarnings(require("data.table"))) { install.packages("data.table") require("data.table")}set.seed(123)n = 5e7k = 5e5x = runif(n)grp = sample(k, n, TRUE)timing = list()# sapplytiming[["sapply"]] = system.time({ lt = split(x, grp)##將數據按隨機數分成不等5萬份的list r.sapply = sapply(lt, function(x) list(sum(x), length(x)), simplify = FALSE)})# lapplytiming[["lapply"]] = system.time({ lt = split(x, grp) r.lapply = lapply(lt, function(x) list(sum(x), length(x)))})# tapplytiming[["tapply"]] = system.time( r.tapply <- tapply(x, list(grp), function(x) list(sum(x), length(x))))# bytiming[["by"]] = system.time( r.by <- by(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE))# aggregatetiming[["aggregate"]] = system.time( r.aggregate <- aggregate(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE))# dplyrtiming[["dplyr"]] = system.time({ df = data_frame(x, grp) r.dplyr = summarise(group_by(df, grp), sum(x), n())})# data.tabletiming[["data.table"]] = system.time({ dt = setnames(setDT(list(x, grp)), c("x","grp")) r.data.table = dt[, .(sum(x), .N), grp]})# 驗證每一種方法的輸出結果是不是都等於5萬組sapply(list(sapply=r.sapply, lapply=r.lapply, tapply=r.tapply, by=r.by, aggregate=r.aggregate, dplyr=r.dplyr, data.table=r.data.table), function(x) (if(is.data.frame(x)) nrow else length)(x)==k)# sapply lapply tapply by aggregate dplyr data.table # TRUE TRUE TRUE TRUE TRUE TRUE TRUE # 列印各種方法消耗的時間as.data.table(sapply(timing, `[[`, "elapsed"), keep.rownames = TRUE )[,.(fun = V1, elapsed = V2) ][order(-elapsed)]# fun elapsed#1: aggregate 109.139#2: by 25.738#3: dplyr 18.978#4: tapply 17.006#5: lapply 11.524#6: sapply 11.326#7: data.table 2.686
其實我覺得這種方法測試並沒有達到真正的公平公正,因為先測試的方法得出的結果保存在內存中,由於數據量比較大,R對象佔用的內存會越來越多,肯定會影響後面的方法的效率,但是從結果上看,data.table仍然當仁不讓成為最高效的方法,可憐我一直喜愛的透視表aggregate()函數效率太低啊,姑且假裝它受了前面的那些壞蛋的影響吧。以上參考jangorecki的答案。
How to join (merge) data frames (inner, outer, left, right)(怎麼關聯(合併)兩個數據框(inner, outer, left, right))?首先我們介紹一下兩種使用data.table的對象關聯的方式:1.轉化為data.table對象後,將第二個表作為第一個表子集關聯,中間使用了setkey函數設置主鍵;2.將數據框對象轉化為data.table對象後仍然使用大家熟悉的merge函數。
df1 = data.frame(CustomerId = c(1:6), Product = c(rep("Toaster", 3), rep("Radio", 3)))df2 = data.frame(CustomerId = c(2L, 4L, 7L), State = c(rep("Alabama", 2), rep("Ohio", 1))) library(data.table)dt1 = as.data.table(df1)dt2 = as.data.table(df2)setkey(dt1, CustomerId)setkey(dt2, CustomerId)#右連接具有主鍵的data.table對象(right outer),即從dt1中取出dt2dt1[dt2]setkey(dt1, NULL)setkey(dt2, NULL)#右連接指定匹配變量data.table對象(right outer)dt1[dt2, on = "CustomerId"]#指定匹配變量的data.table對象左連接(left outer),即從dt2中取出dt1dt2[dt1, on = "CustomerId"]#取交集(inner join)dt1[dt2, nomatch=0L, on = "CustomerId"]#取匹配不到的集合dt1[!dt2, on = "CustomerId"]#merge函數取交集(inner join)merge(dt1, dt2, by = "CustomerId")#merge函數取併集(full outer join)merge(dt1, dt2, by = "CustomerId", all = TRUE)
值得一提的是上面的merge函數使用的data.table對象,而不是原來的數據框,速度提升不少。下面我們使用microbenchmark測試不同的關聯方法的效率,這個包裡microbenchmark函數能夠返回不同方法所用的時間,為了統計上的嚴謹性,microbenchmark可以設定執行的次數,然後返回每種方法所用的平均時間、最大時間等統計指標。下面使用500萬行數據分別測試了使用merge函數、sqldf、dplyr、data.table等表關聯函數的效率。不難發現data.table提供的方法是最快的,速度是其他方法的3倍以上,值得注意的是我們使用的是沒有設定主鍵的data.table方法,如果設定了主鍵速度會更快。
library(microbenchmark)library(sqldf)library(dplyr)library(data.table)n = 5e6set.seed(123)df1 = data.frame(x=sample(n,n-1L), y1=rnorm(n-1L))df2 = data.frame(x=sample(n,n-1L), y2=rnorm(n-1L))dt1 = as.data.table(df1)dt2 = as.data.table(df2)# 取交集(inner join)microbenchmark(times = 10L, base = merge(df1, df2, by = "x"), sqldf = sqldf("SELECT * FROM df1 INNER JOIN df2 ON df1.x = df2.x"), dplyr = inner_join(df1, df2, by = "x"), data.table = dt1[dt2, nomatch = 0L, on = "x"])# Unit: seconds# expr min lq mean median uq max neval cld# base 22.963734 24.051917 25.103055 25.429993 25.938964 26.859334 10 c # sqldf 123.529648 125.671176 131.662259 132.998321 136.487638 136.698750 10 d# dplyr 5.986920 6.224048 6.670272 6.800513 7.056816 7.139099 10 b # data.table 1.770946 1.854327 2.186847 2.138139 2.372884 2.773868 10 a #左連接(left outer)microbenchmark(times = 10L, base = merge(df1, df2, by = "x", all.x = TRUE), sqldf = sqldf("SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.x = df2.x"), dplyr = left_join(df1, df2, by = c("x"="x")), data.table = dt2[dt1, on = "x"])# Unit: seconds# expr min lq mean median uq max neval cld# base 25.140901 29.540645 30.727254 31.384768 32.327207 34.35297 10 b # sqldf 129.685183 137.866304 147.262827 146.519228 153.491764 175.51555 10 c# dplyr 6.374783 7.327687 7.827645 7.562135 7.891212 10.52718 10 a # data.table 1.591136 1.828215 4.953622 1.972619 3.165393 28.01517 10 a
以上參考jangorecki的答案。
Drop columns in R data frame(刪除數據框中的列變量)刪除一個數據框中的一列或多列,或者按存儲在某個變量中的列名稱批量刪除,data.table包中的設定的函數同樣可以完成,只是這種操作費時非常短在不同方法之間幾乎沒有差異。
DT[, c('a','b') := NULL]# ordel <- c('a','b')DT[, (del) := NULL]# orset(DT, j = 'b', value = NULL)within(df, rm(b, a))
需要提及的是在刪除列時,大家不要使用編號索引刪除,例如df[, -c(1,4)]這種形式不建議在代碼中出現,因為別人看不懂你要刪什麼,特別是df的格式一旦發生變化後,恐怕你自己也很難分清當時1和4列到底指的是什麼,儘量使用列名刪除,同樣,當你篩選或提取時也要儘量使用列變量的名稱,而不是它們的索引編號。
Quickly reading very large tables as dataframes in R(R如何快速讀入大型數據表)有很多朋友抱怨R的速度,我總是說當確定你的方法為最優時才有理由抱怨工具,下面的測試足以說明不同的方法之間造成R讀取數據效率的天壤之別。
生成測試數據library(data.table)n=1e6DT = data.table( a=sample(1:1000,n,replace=TRUE), b=sample(1:1000,n,replace=TRUE), c=rnorm(n), d=sample(c("foo","bar","baz","qux","quux"),n,replace=TRUE), e=rnorm(n), f=sample(1:1000,n,replace=TRUE) )DT[2,b:=NA_integer_]DT[4,c:=NA_real_]DT[3,d:=NA_character_]DT[5,d:=""]DT[2,e:=+Inf]DT[3,e:=-Inf]write.table(DT,"test.csv",sep=",",row.names=FALSE,quote=FALSE)cat("File size (MB):",round(file.info("test.csv")$size/1024^2),"\n")
測試數據包括6列100萬行,大小約50M。
######標準的read.table函數system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE)) # 用戶 系統 流逝 # 8.75 0.11 8.86######使用read.csv函數system.time(DF1 <- read.csv("test.csv",stringsAsFactors=FALSE)) # 用戶 系統 流逝 # 6.53 0.12 6.68######data.table包的fread函數require(data.table)system.time(DT <- fread("test.csv")) # 用戶 系統 流逝 # 1.82 0.02 1.87######sqldf包require(sqldf)system.time(SQLDF <- read.csv.sql("test.csv",dbname=NULL))# 用戶 系統 流逝 # 4.17 0.14 4.40######sqldf包另外一種方法f <- file("test.csv")system.time(SQLf <- sqldf("select * from f", dbname = tempfile(), file.format = list(header = T, row.names = F)))# 用戶 系統 流逝 # 4.21 0.23 4.45
不難看出,data.table包fread函數的效率是其他方法的兩倍以上。以上基於mnel答案。
Remove rows with NAs in data.frame(移除數據框中含有缺失值的行)data.table包裡有na.omit函數,可以刪除包含有缺失值的行,但是它遠沒有complete.cases函數有用,後者不僅可以完成刪除任務,而且可以篩選。
final[complete.cases(final),]na.omit(final)
後期data.table裡的函數如果使用索引查找,可能會優化這一任務的內存佔用。
Drop factor levels in a subsetted data frame(刪除分組後的數據框多餘的因子水平)有時通過篩選獲得了某個數據框的子集,但是如果數據框中存在因子變量的話,子集會繼承原來數據框中所有的因子水平,即使它們在子集中不存在。如果用這個子集做圖進行facet或者table或者數據框join時可能出現不必要的麻煩。
library(data.table)dt = data.table(letters=factor(letters[1:5]), numbers=seq(1:5))levels(dt$letters)#[1] "a" "b" "c" "d" "e"subdt = dt[numbers <= 3]levels(subdt$letters)#[1] "a" "b" "c" "d" "e"upd.cols = sapply(subdt, is.factor)subdt[, names(subdt)[upd.cols] := lapply(.SD, factor), .SDcols = upd.cols]levels(subdt$letters)#[1] "a" "b" "c"
是的,沒有效率上的改善,因為這裡有一個使用factor函數再造因子變量的過程,現在的方法都無法避開。
R list to data frame(將list轉化為數據框)將一個list的不同元素包含的內容按行組合為一個新的數據框。其實,完成數據對象之間的轉換是數據分析師的基本技能,除了list轉化為data.frame外,你還要學會data.frameh轉化為list,list轉corpus,因子轉字符等等,這些將在本書的基礎章節講解,我們現在注意的是完成這個任務不同方法是否存在效率上的差異。
我一般使用基礎的do.call和rbind函數,如果list各個元素的長短不一,也要選擇不同的處理方法,參看LDA章節。但是data.table裡的rbindlist不僅解決了長短不一(使用fill填充)的問題,而且還可以根據list中元素的名稱匹配後再按行粘貼,當然這些方法如果都設置使用的話會影響其效率,但無論如何均比基礎包裡的do.call和rbind函數組合效率高。
require(data.table)set.seed(1L)names = paste0("V", 1:500)cols = 500Lfoo <- function() { data = as.data.frame(setDT(lapply(1:cols, function(x) sample(10)))) setnames(data, sample(names))}n = 10e3Lll = vector("list", n)for (i in 1:n) { .Call("Csetlistelt", ll, i, foo())}
上面創建了一個list,包含10000個10×500的數據框,下面使用不同方法將這個list轉化為數據框。
測試效率system.time(ans1 <- rbindlist(ll))# 用戶 系統 流逝 # 1.92 0.11 2.06system.time(ans1 <- rbindlist(ll, use.names=TRUE))#用戶 系統 流逝 #3.20 0.22 3.53 system.time(ans2 <- do.call("rbind", ll))# 用戶 系統 流逝 # 1009.85 7.46 1033.80 identical(ans1, setDT(ans2)) # [1] TRUE
rbindlist函數的速度是do.call和rbind函數組合500倍,以上基於Arun的回答。
當你的方法讓機器和人遍體鱗傷時,主動去重現發現也許能夠暫解燃眉之急。
Convert data.frame columns from factors to characters(將數據框中的因子變量轉化為字符變量)這個問題之所以存在是因為基礎包的data.frame()函數默認情況下將字符變量轉化為因子變量,data.table()函數默認情況下是保留字符變量的類型,因此這個問題在data.table看來根本就不存在,當然,使用data.frame()類函數,每次在讀取或轉化為data.frame的時候預先設置stringsAsFactors = FALSE也能避免這個問題。
dt = data.table(col1 = c("a","b","c"), col2 = 1:3)sapply(dt, class)
當然如果你的數據集確實有些因子變量需要轉化為字符變量,也可以按照下面的方法完成。
library(data.table)dt = data.table(col1 = factor(c("a","b","c")), col2 = 1:3)sapply(dt, class)# col1 col2 # "factor" "integer" upd.cols = sapply(dt, is.factor)dt[, names(dt)[upd.cols] := lapply(.SD, as.character), .SDcols = upd.cols]sapply(dt, class)
沒有效率的提升,原因和上個問題一樣,但是這些問題通常不是代碼工作效率瓶頸的所在。
Changing column names of a data frame in R(數據框重命名列名)在數據整理階段,通常要修改一下數據集裡的列名,除了常見的修改方法外,data.table包的setnames函數也能用於修改數據框中的列名。
library(data.table)set.seed(123)n = 1e8df = data.frame(bad=sample(1:3, n, TRUE), worse=rnorm(n))address(df)#[1] "00000000287D5318"colnames(df) <- c("good", "better")address(df)#[1] "000000001BB62FF0"rm(df)dt = data.table(bad=sample(1:3, n, TRUE), worse=rnorm(n))address(dt)#[1] "0000000018EFB700"setnames(dt, c("good", "better"))address(dt)#[1] "0000000018EFB700"rm(dt)setnames(dt, c("good", "better"))
這類操作都是瞬間完成的事,所以並沒有速度上的提升,但是值得注意的是setnames採用索引的方式修改,新加佔用內存要比常見方法少,可以通過address函數看看對象的內存指針地址,如果內存指針地址發生改變,說明上部操作有可能複製了對象,如果沒有改變就是在原有對象上的修改。如果你的內存比較緊俏時,還是選用data.table提供的方法比較合適。
沒有找到你的問題的解決方案?沒關係,因為我們畢竟只是挑選了個別常見的問題而已,你可以提問。通過和很多夥計的接觸,大家都在抱怨自己的問題沒有人幫助解決。其實這個問題要先從自身找原因,很多情況下是因為提問的方式和表述不清(不要羞於承認這一點,如果它真的存在),關於怎麼以可再現的方式提問,我們另作討論。
精通即為方法的再發現通過上面的例子大家應該比較深刻的感受到data.table提供的解決方案帶來的效率上的快速提升。其實我想說的是不僅僅局限於data.table,在數據分析數據挖掘的過程中,不斷的發現新方法創造新效率才是進步的階梯。不管是你多麼熟悉多麼膜拜的方法,如果他讓你在某些工作面前捉襟見肘手足無措時,就應該勇於替換掉它們,尋求新的技能。
遺憾的是限於篇幅,上面的問題並沒有完全包括數據分析常見的內容,例如數據整形(dcast、melt)、重疊關聯(overlapping joins)、滾動關聯(rolling joins)、二分檢索(binary search)等等,將會在相應的章節講解。
作為讀書人,我也看過不少r的書,個人比較崇洋媚外,國內的好書還是比較少,多是些個應景之作。曾經有人建議我將書按章節分為入門、進階和精通幾個篇章,我看其他作者的寫法,大多把一些基礎操作放在入門篇章,把算法和一些小實例項目放在進階和精通篇章(基於項目進程中的分法)。我個人不待見這種偷懶的方法,實際上很多精通和進階的內容都是對入門時基礎操作的顛覆,而不是上面基於項目進程中的分法。即使在項目進程中,基礎的數據清洗和整形的重要性、難度、耗時都比後面的算法選擇之類的操作要高出好幾個釐米。所以在本書中將精通定義為解決方案的重新發現。
選自《數據挖掘之道:基於R和python的實戰之旅》
好主意值得擴散,激發我們創造的動力,非常感謝花粉傳播者