【问题标题】:Speeding Up Parsing Text to data.table in R [duplicate]加快将文本解析为 R 中的 data.table [重复]
【发布时间】:2016-11-28 03:27:55
【问题描述】:

我使用read.table() 将一个文本文件读入 R 中,生成一个包含一列未解析数据的数据框。

我还有一个数据字典,其中包含文本文件每一行的列名及其开始和结束字符位置。

这是我用一个小例子解析文本文件的方法:

library(data.table)

df <- data.frame(
  parse=c("123qweASD","234werSDF","345ertDFG"),
  stringsAsFactors = FALSE
)

guide <- data.frame(
 name=c("c1","c2","c3"),
 begin=c(1,3,6),
 end=c(2,5,9)
)

emptyDF <- data.frame(matrix(ncol = nrow(guide), 
                             nrow = nrow(df)))
colnames(emptyDF) <- as.character(unlist(guide[1]))
emptyDF[is.na(emptyDF)] <- ""

setDT(emptyDF)

for(y in 1:nrow(df)){
  split <- character()
  for(z in 1:nrow(guide)){
    extr <- substr(df[y,], guide[z, 2], guide[z, 3])
    split <- c(split, extr)
  }
  emptyDF <- emptyDF[y, names(emptyDF) := as.list(split)]
}

导致:

> emptyDF
   c1  c2   c3
1: 12 3qw eASD
2: 23 4we rSDF
3: 34 5er tDFG

为了避免追加和加快速度,我创建了一个空的 data.table,其行长与未解析的数据相同,并将其行替换为已解析的行

该方法有效,但运行具有 200,000 行和 90 列的文件需要一段时间。

我还能做些什么来加快速度吗?

【问题讨论】:

    标签: r performance parsing data.table


    【解决方案1】:

    如果您知道文本文件每一行的开始和结束字符位置,那么您绝对应该使用基础 R 中的 ?read.fwffread 没有任何此类功能,但您可以将其转换为 @ 987654324@之后...)

    writeLines(c("123qweASD","234werSDF","345ertDFG"),
                con="tmpfwf.txt")
    guide <- data.frame(
        name=c("c1","c2","c3"),
        begin=c(1,3,6),
        end=c(2,5,9)
    )
    
    df <- read.fwf("tmpfwf.txt",guide$end+1-guide$begin)
    data.table::data.table(df)
    ##    V1  V2   V3
    ## 1: 12 3qw eASD
    ## 2: 23 4we rSDF
    ## 3: 34 5er tDFG
    

    【讨论】:

    • read.fwf 快了很多。非常感谢!
    • 也许发布时间,作为补充答案?我没有费心构建一个实际大小的示例,但我认为这将是有用的信息
    • 我同意!刚刚发布了。
    • @Warner 查看我的回答here,了解许多固定宽度输入选项的基准
    【解决方案2】:

    readr::read_fwf 呢?它更快且对错误具有鲁棒性。

    Warner 采用大数据集:

    writeLines(as.character(unlist(df1[1])), con="df1.txt")
    system.time({
      a4 <- read_fwf("df1.txt", fwf_widths(guide1$end+1-guide1$begin))
    })
    
      user  system elapsed 
    4.398    0.390   4.975
    
    system.time({
      a2 <- read.fwf("df1.txt", guide1$end+1-guide1$begin)
    })
    
       user  system elapsed 
    101.583  83.111 190.462
    
    system.time({
    a3 <- setDT(setNames(do.call(rbind.data.frame, 
                         Map(substring, df1$v1, list(guide1$begin), list(guide1$end))), guide1$names))[]
    })
    
       user  system elapsed 
    140.660   2.790 147.322
    

    这最后的时间被搞砸了,因为它没有从文件中读取数据。它从内存中读取它。要真正比较时间,您还需要将readLines 之类的时间添加到其中。

    【讨论】:

    • 这比base-R函数有什么优势吗?
    • 接受了这个答案,因为我有很多大型数据集,而且速度非常快。
    【解决方案3】:

    编辑以包含第三种方法

    比较我的方法、@Ben Bolker 和 @akrun 的方法的时间安排。我通常不比较速度,因此可能有一种更有说服力的设置方式。接受建议。

    我创建了一个包含 500 行的示例和一个包含 250,000 行的示例。我看看拆分成 10 列和 100 列所需的时间。

    方法 1: 使用 for 循环填充空的 data.table()

    方法二: read.fwf()

    方法 3: setDT()substring

    这是这些方法所需的时间

    > results
                       DataSize   Approach1   Approach2   Approach3
    1      500 Rows, 10 Columns  0.01934705 0.002605216 0.001200851
    2     500 rows, 100 Columns  0.07892265 0.028603617 0.014927268
    3  250,000 Rows, 10 Columns  6.84735728 1.527935565 1.585325948
    4 250,000 rows, 100 Columns 37.34443290 8.075678647 4.172232886
    

    read.fwf() 显然比我在方法中使用的 for 循环快。 substring 方法是最快的。有趣的是,这三种方法的缩放比例取决于列数和行数:

    > # Time factor increase with column and row increases
    > scaling
                                         Increase  Approach1  Approach2   Approach3
    1   500 Rows: Increase from 10 to 100 Columns   4.079311  10.979366   12.430577
    2  250k Rows: Increase from 10 to 100 Columns   5.453846   5.285353    2.631782
    3  10 Columns: Increase from 500 to 250k Rows 353.922518 586.490999 1320.168952
    4 100 Columns: Increase from 500 to 250k Rows 473.177640 282.330677  279.504118
    

    当存在少量列或少量行时,空的data.table 方法似乎比read.fwf()substring 方法具有更好的扩展性。有什么想法吗?

    另一个想法:我的数据集的列和行比这里最大的例子少一点。但是解析花了将近一个小时。我的数据集中的每一行有 700-800 个字符,结果列的大小不同。这是另一个值得考虑的性能和速度维度。

    我是这样设置的。

    设置带有随机字符串的表格和引导表格

    library(stringi)
    
    df1 <- data.frame(
      v1=stri_rand_strings(n=250000, length=200, pattern="[A-Za-z0-9]"),
      stringsAsFactors=FALSE
    )
    df2 <- as.data.frame(df1[1:500,])
    
    guide1 <- data.frame(
      names=paste0(rep("c",100), 1:100),
      begin=(1:100)*2-1,
      end=(1:100)*2,
      stringsAsFactors = FALSE
    )
    
    guide2 <- data.frame(
      names=paste0(rep("c",10), 1:10),
      begin=(0:9)*20+1,
      end=(1:10)*20,
      stringsAsFactors = FALSE
    )
    

    为两种方法设置函数

    approach1 <- function(emptydf, df, guide){
      for(y in 1:nrow(df)){
        split <- character()
        for(z in 1:nrow(guide)){
          extr <- substr(df[y,], guide[z, 2], guide[z, 3])
          split <- c(split, extr)
        }
        emptydf <- emptydf[y, names(emptydf) := as.list(split)]
      }
      return(emptydf)
    }
    
    
    approach2 <- function(path, guide){
      import <- read.fwf(path, guide$end+1-guide$begin)
    }
    
    
    approach3 <- function(df, guide){
          setDT(setNames(do.call(rbind.data.frame, Map(substring, df$v1, 
                      list(guide$begin), list(guide$end))), guide$names))[] 
    
    }
    

    方法 1:带有 For 循环的空 data.table:

    emptydf1 <- data.frame(matrix(ncol = nrow(guide1), 
                                  nrow = nrow(df1)))
    colnames(emptydf1) <- as.character(unlist(guide1[1]))
    emptydf1[is.na(emptydf1)] <- ""
    
    emptydf2 <- as.data.frame(emptydf1[, 1:10])
    emptydf3 <- as.data.frame(emptydf1[1:500,])
    emptydf4 <- as.data.frame(emptydf1[1:500,1:10])
    
    setDT(emptydf1)
    setDT(emptydf2)
    setDT(emptydf3)
    setDT(emptydf4)
    
    ## 500 rows and 10 columns
    a0 <- Sys.time()
    app1Out1 <- approach1(emptydf4, df2, guide2)
    a1 <- Sys.time()
    ## 500 rows and 100 columns
    b0 <- Sys.time()
    app1Out2 <- approach1(emptydf3, df2, guide1)
    b1 <- Sys.time()
    ## 250,000 rows and 10 columns
    c0 <- Sys.time()
    app1Out3 <- approach1(emptydf2, df1, guide2)
    c1 <- Sys.time()
    ## 250,000 rows and 100 columns
    d0 <- Sys.time()
    app1Out4 <- approach1(emptydf1, df1, guide1)
    d1 <- Sys.time()
    

    方法二: read.fwf()

    writeLines(as.character(unlist(df1[1])), con="df1.txt")
    writeLines(as.character(unlist(df2[1])), con="df2.txt")
    
    ## 500 rows and 10 columns
    e0 <- Sys.time()
    app2Out1 <- approach2("df2.txt", guide2)
    e1 <- Sys.time()
    ## 500  rows and 100 columns
    f0 <- Sys.time()
    app2Out2 <- approach2("df2.txt", guide1)
    f1 <- Sys.time()
    ## 500 rows and 10 columns
    g0 <- Sys.time()
    app2Out3 <- approach2("df1.txt", guide2)
    g1 <- Sys.time()
    ## 250,00 rows and 100 columns
    h0 <- Sys.time()
    app2Out4 <- approach2("df1.txt", guide1)
    h1 <- Sys.time()
    

    方法 3: setDF()substring

    names(df2) <- "v1"
    
    ## 500 rows and 10 columns
    i0 <- Sys.time()
    app3Out1 <- approach3(df2, guide2)
    i1 <- Sys.time()
    ## 500 rows and 100 columns
    j0 <- Sys.time()
    app3Out2 <- approach3(df2, guide1)
    j1 <- Sys.time()
    ## 250,000 rows and 10 columns
    k0 <- Sys.time()
    app3Out3 <- approach3(df1, guide2)
    k1 <- Sys.time()
    ## 250,000 rows and 100 columns
    l0 <- Sys.time()
    app3Out4 <- approach3(df1, guide1)
    l1 <- Sys.time()
    

    设置结果表

    tests <- c("500 Rows, 10 Columns","500 rows, 100 Columns","250,000 Rows, 10 Columns",
               "250,000 rows, 100 Columns")
    app1 <- c(as.numeric(a1-a0)/60,as.numeric(b1-b0)/60,as.numeric(c1-c0),as.numeric(d1-d0))
    app2 <- c(as.numeric(e1-e0)/60,as.numeric(f1-f0)/60,as.numeric(g1-g0),as.numeric(h1-h0))
    app3 <- c(as.numeric(i1-i0)/60,as.numeric(j1-j0)/60,as.numeric(k1-k0),as.numeric(l1-l0))
    
    results <- data.frame(
      "DataSize"=tests,
      "Approach1"=app1,
      "Approach2"=app2,
      "Approach3"=app3
    )
    
    # Time factor with increase with column and row increases
    scaling <- data.frame(
      "Increase"=c("500 Rows: Increase from 10 to 100 Columns","250k Rows: Increase from 10 to 100 Columns",
                   "10 Columns: Increase from 500 to 250k Rows","100 Columns: Increase from 500 to 250k Rows"),
      "Approach1"=c((results[2,2]/results[1,2]),(results[4,2]/results[3,2]),
                    (results[3,2]/results[1,2]),(results[4,2]/results[2,2])),
      "Approach2"=c((results[2,3]/results[1,3]),(results[4,3]/results[3,3]),
                    (results[3,3]/results[1,3]),(results[4,3]/results[2,3])),
      "Approach3"=c((results[2,4]/results[1,4]),(results[4,4]/results[3,4]),
                    (results[3,4]/results[1,4]),(results[4,4]/results[2,4]))
      )
    

    【讨论】:

    • 最后一种情况,results在哪里?
    • 刚刚添加了您的方法。
    • 你花时间做这件事真是太棒了。 mircrobenchmark 包将来可能会帮助您。
    • 您能否在基准测试中添加readr::read_fwf(请参阅我的回答)
    【解决方案4】:

    这是substring的另一个选项

    library(data.table)
    setDT(setNames(do.call(rbind.data.frame, Map(substring, df$parse, 
             list(guide$begin), list(guide$end))), guide$name))[] 
    #   c1  c2   c3
    #1: 12 3qw eASD
    #2: 23 4we rSDF
    #3: 34 5er tDFG
    

    【讨论】:

    • 这也行得通 - 我必须将其包括在性能和速度比较中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-05
    • 2011-02-28
    相关资源
    最近更新 更多