【问题标题】:Efficient filtering through multiple columns by group按组有效过滤多个列
【发布时间】:2021-08-27 13:08:49
【问题描述】:

假设一个数据集包含每个 ID 多行和多列,其中包含一些存储为字符串的代码:

df <- data.frame(id = rep(1:3, each = 2),
                 var1 = c("X1", "Y1", "Y2", "Y3", "Z1", "Z2"),
                 var2 = c("Y1", "X2", "Y2", "Y3", "Z1", "Z2"),
                 var3 = c("Y1", "Y2", "X1", "Y3", "Z1", "Z2"),
                 stringsAsFactors = FALSE)

  id var1 var2 var3
1  1   X1   Y1   Y1
2  1   Y1   X2   Y2
3  2   Y2   Y2   X1
4  2   Y3   Y3   Y3
5  3   Z1   Z1   Z1
6  3   Z2   Z2   Z2

现在,假设我想过滤掉任何相关列中具有特定代码(此处为 X)的所有 ID。使用dplyrpurrr,我可以:

df %>%
 group_by(id) %>%
 filter(all(reduce(.x = across(var1:var3, ~ !grepl("^X", .)), .f = `&`)))

     id var1  var2  var3 
  <int> <chr> <chr> <chr>
1     3 Z1    Z1    Z1   
2     3 Z2    Z2    Z2 

它运行良好,结构紧凑且易于理解,但是,对于大型数据集(数百万个 ID 和数千万个观察值),它的效率相当低。我欢迎任何关于使用任何库计算更高效代码的想法。

【问题讨论】:

    标签: r regex dataframe performance filtering


    【解决方案1】:

    一些可能的速度点

    • 尝试使用 group by 之类的方法,即 dplyr 中的 group_bydata.table 中的 by = ,因为这会降低您的整体性能
    • 如果你有固定的目标模式,例如,以X 开头,那么substr 可能比grepl 更有效,模式为^X

    一些基本 R 方法

    看来我们可以在@Waldi's fastest approach的基础上通过下面的进一步加速

    TIC1 <- function() {
        subset(df, ave(rowSums(substr(as.matrix(df[, -1]), 1, 1) == "X") == 0, id, FUN = all))
    }
    

    TIC2 <- function() {
        subset(df, !id %in% id[rowSums(substr(as.matrix(df[, -1]), 1, 1) == "X") > 0])
    }
    

    TIC3 <- function() {
        subset(df, !id %in% id[do.call(pmax, lapply(df[-1], function(v) substr(v, 1, 1) == "X")) > 0])
    }
    

    基准测试

    @Waldi@EnricoSchumann 的回答相比:

    microbenchmark(
        TIC1(),
        TIC2(),
        TIC3(),
        fun1(),
        fun2(),
        waldi_speed(),
        unit = "relative"
    )
    
    Unit: relative
              expr       min        lq      mean    median        uq       max
            TIC1()  3.385215  3.451424  3.488670  3.569668  3.684895  3.618991
            TIC2()  1.062116  1.084568  1.074789  1.090400  1.114443  1.027673
            TIC3()  1.077660  2.208734  2.185960  2.214180  2.293366  2.141994
            fun1()  1.166342  1.155096  1.169574  1.153223  1.207932  1.405530
            fun2()  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000
     waldi_speed() 26.218953 26.560429 26.373054 26.952997 27.396017 26.333575
     neval
       100
       100
       100
       100
       100
       100
    

    给定

    n <- 5e4
    df <- data.frame(
        id = rep(1:(n / 2), each = 2, length.out = n),
        var1 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        var2 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        var3 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        stringsAsFactors = FALSE
    )
    
    TIC1 <- function() {
        subset(df, ave(rowSums(substr(as.matrix(df[, -1]), 1, 1) == "X") == 0, id, FUN = all))
    }
    
    TIC2 <- function() {
        subset(df, !id %in% id[rowSums(substr(as.matrix(df[, -1]), 1, 1) == "X") > 0])
    }
    
    TIC3 <- function() {
        subset(df, !id %in% id[do.call(pmax, lapply(df[-1], function(v) substr(v, 1, 1) == "X")) > 0])
    }
    
    
    waldi_speed <- function() {
        setDT(df)
        df[df[, .(keep = .I[!any(grepl("X", .SD))]), by = id, .SDcols = patterns("var")]$keep]
    }
    
    
    repeated_or <- function(...) {
        L <- list(...)
        ans <- L[[1L]]
        if (...length() > 1L) {
              for (i in seq.int(2L, ...length())) {
                    ans <- ans | L[[i]]
                }
          }
        ans
    }
    
    fun1 <- function() {
        ## using a pattern
        m <- lapply(df[, -1], grepl, pattern = "^X", perl = TRUE)
        df[!df$id %in% df$id[do.call(repeated_or, m)], ]
    }
    
    fun2 <- function() {
        ## using a fixed string
        m <- lapply(df[, -1], function(x) substr(x, 1, 1) == "X")
        df[!df$id %in% df$id[do.call(repeated_or, m)], ]
    }
    

    【讨论】:

    • 太棒了!我开始对data.table 失去信任 ;-) 赞成!
    • @Waldi data.table 本身很快,但by = 是瓶颈:)
    • @ThomsIsCoding,另一个相当高效/优雅的:subset(df, !id %in% id[Reduce('|',lapply(df[,-1],function(x) substr(x,1,1)=='X'))])
    • @Waldi 是的,只要列不多,它就很有效:)
    【解决方案2】:

    这是另一种tidyverse 方法。

    my_fun <- function(.data) {
      .data %>% 
        group_by(id) %>% 
        filter(!grepl("X", paste(var1, var2, var3, collapse = ""))) %>% 
        ungroup()
    }
    
    my_fun(df)
    
    # # A tibble: 2 x 4
    #      id var1  var2  var3 
    #   <int> <chr> <chr> <chr>
    # 1     3 Z1    Z1    Z1   
    # 2     3 Z2    Z2    Z2   
    
    df_fun <- function(.data) {
      .data %>%
        group_by(id) %>%
        filter(all(reduce(.x = across(var1:var3, ~ !grepl("^X", .)), .f = `&`))) %>% 
        ungroup()
    }
    
    performance <- bench::mark(
      my_fun(df),
      df_fun(df)
    )
    
    performance %>% select(1:4)
    
    # # A tibble: 2 x 4
    #   expression       min   median `itr/sec`
    #   <bch:expr>  <bch:tm> <bch:tm>     <dbl>
    # 1 my_fun(df)    2.6ms    2.7ms      364.
    # 2 df_fun(df)    6.01ms   6.39ms      152.
    

    【讨论】:

    • 我将赏金授予@ThomasIsCoding,因为他的解决方案是最有效的,但我接受了您的回答,因为它非常适合我的管道,而且对于我目前的工作来说,它的效率相当高。
    • @tmfmnk 如果你看一下时序,GKi 的解决方案目前是最有效的,其次是 enrico-schumann,然后是 ThomasIsCoding。
    【解决方案3】:

    另外两个data.table 解决方案:

    library(data.table)
    setDT(df)
    df[,.SD[!any(grepl("X", .SD))],by=id,.SDcols=patterns('var')]
    
       id var1 var2 var3
    1:  3   Z1   Z1   Z1
    2:  3   Z2   Z2   Z2
    

    可以是improved for speed,但会降低可读性:

    df[df[, .(keep=.I[!any(grepl("X", .SD))]), by=id,.SDcols=patterns('var')]$keep]
    

    基准测试:

    n <- 1e4
    df <- data.frame(id = rep(1:(n/2), each = 2,length.out=n),
                     var1 = mapply(paste0,LETTERS[23+sample(1:3,n,replace=T)],sample(1:3,n,replace=T)),
                     var2 = mapply(paste0,LETTERS[23+sample(1:3,n,replace=T)],sample(1:3,n,replace=T)),
                     var3 = mapply(paste0,LETTERS[23+sample(1:3,n,replace=T)],sample(1:3,n,replace=T)),
                     stringsAsFactors = FALSE) 
    
    
    Unit: milliseconds
              expr       min         lq        mean    median         uq       max neval
             ref() 2131.5304 2285.54535 2401.612346 2367.8145 2480.10490 3294.9647   100
      TeamTeaFan() 1760.1280 1918.29075 1986.489995 1967.7518 2029.02090 2858.8118   100
           ronak()  289.1461  306.06050  324.418149  314.4888  333.44100  468.1077   100
            anil()  230.5183  244.04175  259.687656  255.4336  267.69550  370.5758   100
           waldi()  226.5081  238.23055  256.824345  251.8372  267.23395  384.6071   100
     waldi_speed()   41.0354   45.12365   51.428189   48.6736   55.20530  155.4654   100
             zaw()   25.9210   28.96225   33.508240   31.2333   37.77565   49.5777   100
             TIC()    3.9299    4.51920    5.295555    4.8717    5.43565   14.7225   100
    

    【讨论】:

    • 出于好奇,如果您能将我的方法if_all 包含在基准测试中,那就太好了。看到 @Zaw 的基本 R 方法优于 {data.table} 真的很惊讶!
    • 谢谢!超级有趣的基准。预计新的if_all 会更快!
    • 干得好,点赞!实际上性能可以进一步提高(见我的回答)
    【解决方案4】:

    另一个基本 R 解决方案,使用 ThomasIsCoding 提供的代码示例。 首先,定义一个辅助函数:

    repeated_or <- function(...) {
        L <- list(...)
        ans <- L[[1L]]
        if (...length() > 1L)
            for (i in seq.int(2L, ...length()))
                ans <- ans | L[[i]]
        ans
    }
    

    它将采用任意数量的逻辑向量x1x2x3,...并产生x1 | x2 | x3 ...等等。

    实际工作由以下函数完成,有两种变体。 该函数假定要搜索除第一列之外的所有列。

    fun1 <- function() {
        ## using a pattern
        m <- lapply(df[, -1], grepl, pattern = "^X", perl = TRUE)
        df[!df$id %in% df$id[do.call(repeated_or, m)], ]
    }
    
    fun2 <- function() {
        ## using a fixed string
        m <- lapply(df[, -1], function(x) substr(x, 1,1) == "X")
        df[!df$id %in% df$id[do.call(repeated_or, m)], ]
    }
    

    现在,使用 ThomasIsCoding 提供的代码:

    n <- 1e4
    df <- data.frame(
        id = rep(1:(n / 2), each = 2, length.out = n),
        var1 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        var2 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        var3 = mapply(paste0, LETTERS[23 + sample(1:3, n, replace = T)], sample(1:3, n, replace = T)),
        stringsAsFactors = FALSE
    )
    
    library("microbenchmark")
    microbenchmark(
        fun1(),
        fun2(),
        TIC1(),
        TIC2(),
        waldi_speed(),
        unit = "relative"
    )
    ## Unit: relative
    ##           expr       min        lq      mean    median        uq       max neval
    ##         fun1()  1.180372  1.183109  1.205269  1.189091  1.187704  1.163667   100
    ##         fun2()  1.000000  1.000000  1.000000  1.000000  1.000000  1.000000   100
    ##         TIC1()  3.487775  3.462417  3.549228  3.491580  3.494310  2.857216   100
    ##         TIC2()  1.140145  1.131872  1.141466  1.146900  1.142863  1.078746   100
    ##  waldi_speed() 31.440025 30.845971 30.556054 30.798701 30.338251 26.213920   100
    

    【讨论】:

    • 我不认为这会提供所需的输出。你可以试试OP的df
    • @ThomasIsCoding:是的,你是对的;谢谢。我只删除了包含X 的行。我已经更新了答案。
    【解决方案5】:
    • 特殊功能:您可能会进行许多操作以查看是否找到特定代码。对这种类型使用专用函数可能比一般函数更快。 startsWith(x, "X") 会比 grepl("^X", x) 快。

    • 子集:如果查找特定代码的函数很慢(操作比子集慢),请只进行此操作对于代码尚未找到的行中的其余列

    • Hash Lookup:如果有相同id 的行有命中,则需要比较所有剩余的没有直接命中的id。因此,列表中的 lookup 包含命中的 id,应该很快。使用像fastmatch::fmatch 这样的哈希表,这种查找可能会很快。

    • 存储类型:如果data.frame 的列具有所有相同类型,则将其存储在matrix 中时对其的操作可能会更快list

    • 避免重新排列数据:尽量按原样使用数据。避免像 splitgroup 这样会重新排列当前数据的操作。


    您可以unlist df[-1] 并测试它是否为startsWith X,创建一个matrixdfnrow 并取rowSums,如果它是&gt;0 id 成功了。我将那些id 存储在i 中。可选的unique id's 可以计算。现在测试id 是否为%in% i 并使用! 取反。 %in% 的一个可能更快的替代方案是来自 fastmatch%fin%

    i <- df$id[unlist(df[-1], FALSE, FALSE) |>
                 startsWith("X") |>
                 matrix(nrow(df)) |>
                 rowSums() > 0]
    #i <- unique(i)       #Optional
    #i <- kit::funique(i) #Optional faster unique
    df[!df$id %in% i,]
    #  id var1 var2 var3
    #5  3   Z1   Z1   Z1
    #6  3   Z2   Z2   Z2
    
    library(fastmatch)
    df[!df$id %fin% i,]
    

    lappyl 上使用i 并在Reduce 中使用| 或在Reduce 变慢的情况下使用evalstr2langpaste 一起使用的另一种方法:

    i <- lapply(df[,-1], startsWith, "X")
    i <- df$id[Reduce(`|`, i)]
    #i <- eval(str2lang(paste0("i[[", seq_along(i), "]]", collapse = "|"))) #Alternative to Reduce
    df[!df$id %in% i,]
    

    还可以测试它是否以X 开头,仅在那些没有命中的情况下,并仅对那些没有命中X 的行使用%in%,这将是有意义的当子集比测试以X 开头并且子集比寻找匹配更快时。

    i <- Reduce(function(x, y) `[<-`(x,!x,startsWith(y[!x], "X")),
           df[,-1], logical(nrow(df)))
    i[!i] <- df$id[!i] %in% df$id[i]
    df[!i,]
    

    基于@Waldi 的基准测试,使用来自@thomasiscoding 的TIC2() 和来自@enrico-schumann 的fun2()

     getDf <- function(nr, nc) { #function to creat example dataset
        data.frame(id = sample(seq_len(nr/5), nr, TRUE),
          lapply(setNames(seq_len(nc), paste0("var", seq_len(nc))),
            function(i) paste0(sample(LETTERS, nr, TRUE), sample(0:9, nr, TRUE))))
    }
    
    library(fastmatch)
    FGKi1 <- function() {
      df[!df$id %in% df$id[rowSums(matrix(startsWith(unlist(df[-1], FALSE, FALSE),
                                                     "X"), nrow(df))) > 0],]}
    FGKi2 <- function() {
      df[!df$id %in% unique(df$id[rowSums(matrix(startsWith(unlist(df[-1],
                                     FALSE, FALSE), "X"), nrow(df))) > 0]),]}
    FGKi3 <- function() {
      df[!df$id %fin% df$id[rowSums(matrix(startsWith(unlist(df[-1], FALSE, FALSE),
                                                      "X"), nrow(df))) > 0],]}
    FGKi4 <- function() {
      df[!df$id %in% df$id[Reduce(`|`, lapply(df[, -1], startsWith, "X"))],]
    }
    FGKi5 <- function() {
      df[!df$id %fin% df$id[Reduce(`|`, lapply(df[, -1], startsWith, "X"))],]
    }
    FGKi6 <- function() {
      i <- Reduce(`|`, lapply(df[, -1], startsWith, "X"))
      i[!i] <- df$id[!i] %in% df$id[i]
      df[!i,]
    }
    FGKi7 <- function() {
      i <- lapply(df[, -1], startsWith, "X")
      i <- eval(str2lang(paste0("i[[", seq_along(i), "]]", collapse = "|")))
      df[!df$id %fin% df$id[i],]
    }
    repeated_or <- function(...) {
        L <- list(...)
        ans <- L[[1L]]
        if (...length() > 1L)
            for (i in seq.int(2L, ...length()))
                ans <- ans | L[[i]]
        ans
    }
    fun2 <- function() {
        ## using a fixed string
        m <- lapply(df[, -1], function(x) substr(x, 1,1) == "X")
        df[!df$id %in% df$id[do.call(repeated_or, m)], ]
    }
    TIC2 <- function() {
        subset(df, !id %in% id[rowSums(substr(as.matrix(df[, -1]), 1, 1) == "X") > 0])
    }
    
    set.seed(42)
    df <- getDf(1e5, 3) #3 col wide Table
    bench::mark(TIC2(), fun2(), FGKi1(), FGKi2(), FGKi3(), FGKi4(),
       FGKi5(), FGKi6(), FGKi7())
    #  expression     min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
    #  <bch:expr> <bch:t> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
    #1 TIC2()      24.7ms  24.9ms      40.2   15.07MB    112.      5    14      125ms
    #2 fun2()      22.3ms  22.5ms      43.9   11.26MB     39.9    11    10      251ms
    #3 FGKi1()     14.6ms    15ms      66.8   12.78MB     58.9    17    15      255ms
    #4 FGKi2()     14.9ms  15.1ms      66.3   12.97MB     58.5    17    15      256ms
    #5 FGKi3()     12.1ms  12.3ms      80.8   12.23MB     72.3    19    17      235ms
    #6 FGKi4()     12.7ms  12.9ms      77.7    8.97MB     27.7    28    10      360ms
    #7 FGKi5()     10.2ms  10.3ms      96.4    8.42MB     51.4    30    16      311ms
    #8 FGKi6()     13.2ms  13.3ms      75.1   11.38MB     53.6    21    15      280ms
    #9 FGKi7()     10.3ms  10.4ms      95.2    8.42MB     36.8    31    12      326ms
    
    set.seed(42)
    df <- getDf(1e4, 1e3) #1000 col wide Table
    bench::mark(TIC2(), fun2(), FGKi1(), FGKi2(), FGKi3(), FGKi4(),
       FGKi5(), FGKi6(), FGKi7())
    #  expression     min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
    #  <bch:expr> <bch:t> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
    #1 TIC2()     430.4ms 434.4ms      2.30     230MB     3.45     2     3      869ms
    #2 fun2()     374.6ms 405.6ms      2.47     191MB     6.16     2     5      811ms
    #3 FGKi1()    110.8ms 117.7ms      7.87     191MB    13.8      4     7      509ms
    #4 FGKi2()    108.9ms 111.1ms      8.32     191MB    11.7      5     7      601ms
    #5 FGKi3()    107.8ms 107.8ms      9.25     191MB     9.25     5     5      541ms
    #6 FGKi4()     52.5ms  54.6ms     16.6      115MB    14.7      9     8      543ms
    #7 FGKi5()     52.5ms  54.7ms     18.3      115MB    18.3     10    10      547ms
    #8 FGKi6()     52.8ms  55.2ms     18.1      115MB    16.3     10     9      553ms
    #9 FGKi7()     53.7ms  56.6ms     17.6      115MB    17.6      9     9      510ms
    #Warning message:
    #Some expressions had a GC in every iteration; so filtering is disabled. 
    

    【讨论】:

    • 太棒了!点赞!顺便说一句,如果你有很多列,Reduce 可能会很慢,但FGKi3() 确实令人印象深刻!
    • 我无法观察到,使用 Reduce 的变体在很多 (1000) 列中变得缓慢
    • 好像和你观察到的一样。那么我想撤回我的声明:)
    【解决方案6】:

    这是data.table 的变体 -

    library(data.table)
    cols <- grep('var', names(df))
    
    setDT(df)
    
    df[, .SD[all(!Reduce(`|`, lapply(.SD, grepl, pattern = '^X')))], id, .SDcols = cols]
    
    #   id var1 var2 var3
    #1:  3   Z1   Z1   Z1
    #2:  3   Z2   Z2   Z2
    

    【讨论】:

      【解决方案7】:

      您可以简单地使用cur_data(),使其表现得像一个向量/矩阵,即用as.vector 或更恰当地用as.matrix 包装它

      library(tidyverse)
      
      df %>%
        group_by(id) %>%
        filter(!any(str_detect(as.matrix(cur_data()), 'X')))
      
      #> # A tibble: 2 x 4
      #> # Groups:   id [1]
      #>      id var1  var2  var3 
      #>   <int> <chr> <chr> <chr>
      #> 1     3 Z1    Z1    Z1   
      #> 2     3 Z2    Z2    Z2
      

      或者,如果您只想在选定的列上使用它

      df %>%
        group_by(id) %>%
        filter(!any(grepl('X', as.matrix(select(cur_data(), starts_with('var'))))))
      

      【讨论】:

        【解决方案8】:

        另一种选择是使用新的if_all(或if_any)。要解决上述问题,我们需要将其进一步包装在all

        library(dplyr)
        
        df %>% 
          group_by(id) %>% 
          filter(all(if_all(starts_with("var"),
                            ~ !grepl("^X", .x))))
        
        #> # A tibble: 2 x 4
        #> # Groups:   id [1]
        #>      id var1  var2  var3 
        #>   <int> <chr> <chr> <chr>
        #> 1     3 Z1    Z1    Z1   
        #> 2     3 Z2    Z2    Z2
        

        reprex package (v0.3.0) 于 2021-06-14 创建

        【讨论】:

          【解决方案9】:

          如果 'id' 始终是第一列,其余列中的值:

          df[df$id %in% names(which(!tapply(grepl("X", as.matrix(df[-1])),
                                            rep(df[ , 1], ncol(df) - 1), any))), ]
          

          【讨论】:

            【解决方案10】:

            这种方法将所有列与 paste 结合起来,然后依靠 stringr 生成一个向量,其中包含存在“X”的所有 id。

            library(tidyverse)
            library(stringr)
            
            df <- data.frame(id = rep(1:3, each = 2),
                             var1 = c("X1", "Y1", "Y2", "Y3", "Z1", "Z2"),
                             var2 = c("Y1", "X2", "Y2", "Y3", "Z1", "Z2"),
                             var3 = c("Y1", "Y2", "X1", "Y3", "Z1", "Z2"),
                             stringsAsFactors = FALSE)
            
            
            system.time({df %>%
                    group_by(id) %>%
                    filter(all(reduce(.x = across(var1:var3, ~ !grepl("^X", .)), .f = `&`)))})
            #>    user  system elapsed 
            #>   0.022   0.001   0.023
            
            
            #answer 
            system.time({
            criteria <- as.numeric(paste0(df$var1, df$var2, df$var3, '-', df$id) %>%
                            {str_sub(.[str_detect(., 'X')], start = -1)} |>
                            unique())
            
            df_filtered <- filter(df, !id %in% criteria)
            })
            #>    user  system elapsed 
            #>   0.002   0.000   0.001
            
            df_filtered
            #>   id var1 var2 var3
            #> 1  3   Z1   Z1   Z1
            #> 2  3   Z2   Z2   Z2
            

            reprex package (v2.0.0) 于 2021-06-15 创建

            【讨论】:

              【解决方案11】:

              另一种 Base R 解决方案,我还没有看到。利用行数取模来快速返回要删除的行:

              df[!(df$id %in% df$id[(which(df=="X1" | df=="X2") %% nrow(df))]),]
              id var1 var2 var3
              5  3   Z1   Z1   Z1
              6  3   Z2   Z2   Z2
              

              速度很快,以微秒为单位:

              library(microbenchmark)
              microbenchmark(df[!(df$id %in% df$id[(which(df=="X1" | df=="X2") %% nrow(df))]),])
              Unit: microseconds
              min       lq     mean   median       uq     max
              136.601 140.8505 165.7009 145.4515 172.9005 328.801 
              

              【讨论】:

                猜你喜欢
                • 2022-11-02
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-11-01
                • 2021-10-20
                • 2018-11-14
                • 1970-01-01
                相关资源
                最近更新 更多