【问题标题】:Efficient sub-string search over large dataset在大型数据集上进行高效的子字符串搜索
【发布时间】:2017-12-02 23:05:04
【问题描述】:

我有一个大型数据集tPro1(约 500k 点)。如下所示,感兴趣的变量是tPro1$Path

      Path                                  Row      rm                                              
1  >root>aaaa>bbbb>cccc>dddd>hello         1        TRUE
2  >root>aaaa>bbbb>cccc>dddd>greetings     2        TRUE
3  >root>aaaa>bbbb>cccc>dddd>example       3        TRUE
4  >root>iiii>jjjj>kkkk>llll>mmmm          4        TRUE
5  >root>iiii>jjjj>kkkk>nnnn>testing       5        TRUE

我还有一个较小的数据集,我们称之为Sub1,有几十个左右的数据点。它具有比tPro1 更高级别的路径。

     [1] ">root>aaaa>bbbb>cccc>dddd"
     [2] ">root>aaaa>bbbb>eeee>ffff"
     [3] ">root>aaaa>bbbb>gggg>hhhh" 
     [4] ">root>iiii>jjjj>kkkk>llll>mmmm"
     [5] ">root>iiii>jjjj>kkkk>nnnn" 
     [6] ">root>oooo>pppp>qqqq"

我要做的是将tPro1 中较长的路径与Sub1 中的较短路径相关联。 tPro1 是来自Pro0 的一些关键信息的副本。输出Pro0 将是

          Path                                  Short_path                                                    
1  >root>aaaa>bbbb>cccc>dddd>hello         >root>aaaa>bbbb>cccc>dddd
2  >root>aaaa>bbbb>cccc>dddd>greetings     >root>aaaa>bbbb>cccc>dddd
3  >root>aaaa>bbbb>cccc>dddd>example       >root>aaaa>bbbb>cccc>dddd
4  >root>iiii>jjjj>kkkk>llll>mmmm          >root>iiii>jjjj>kkkk>llll>mmmm
5  >root>iiii>jjjj>kkkk>nnnn>testing       >root>iiii>jjjj>kkkk>nnnn

我已经为Sub1 中的每个路径编写了一个循环,grepl 的每个tPro1 以查看它是否是子字符串。对于 500k*24 点,这将是一个非常低效的过程,所以我尝试了一些优化:

  1. 注意tPro1$rm。找到子字符串时,将其设置为 false。之后将它们删除/跳过以节省毫无意义的重新检查时间。
    1. 一个 Path 可能在tPro1 中出现多次。因此,当找到 s 的有效子字符串 p 时,算法不会继续 grepl,而是遍历数据集并查找 s 的所有未检查实例。

我的代码是

start.time <- Sys.time()

for (p in Sub1$Path) {
  for (i in 1:NROW(tPro1)) {
    if (tPro1[i,3]) {
      if (grepl(p, tPro1[i,1], fixed=TRUE)) {
        # Replace all of subpath 
        for (j in i:NROW(tPro1)) {
          if (tPro1[j,1] == tPro1[i,1]) {
            Pro0[tPro1[j,2],2] <- p
            tPro1[j,3] <- FALSE
          }
        }
      }
    }
  }
  v <- unlist(tPro1[,3])
  tPro1 <- tPro1[v,]
}

end.time <- Sys.time()
time.taken <- end.time - start.time
time.taken

处理完整的数据集不会在人工时间内停止(至少在我的机器上)。出于说明目的,一次执行 1000 个批次(减少 tPro1)需要 46 秒。 2000 需要 1 分钟,3000:1.4 分钟。

可以进行任何明显的改进,还是仅仅是问题的本质?

编辑:大约有 54k 条独特的长路径,而且并非所有长路径都有相应的短路径(例如,在 tPro1 中有 &gt;root&gt;strange&gt;path,而在 sub1 中没有路径形式&gt;root&gt;strange)

EDIT2:在下面rosscova 的回答之后,时间从可能永恒缩短到 279.75 秒!

【问题讨论】:

  • 你也可以发送一些数据吗(比如链接或smth?)
  • 字符串&gt;root&gt;aaaa&gt;bbbb&gt;cccc&gt;dddd&gt;hello 是否总是相同数量的&gt;。我的意思是你能吃吗? &gt;root&gt;aaaa&gt;bbbb&gt;cccc&gt;dddd&gt;xxxx&gt;ΖΖΖΖΖ&gt;hello
  • 不幸的是,我正在使用的数据无法公开,这使得我知道的可重复性变得很尴尬。字符串有可变数量的 >'s
  • 酷!所以让我知道一件事。你只需要知道大表是否包含小表的字符串,对吗?
  • 是的,我也看不到循环的用途...将其转换为 data.table.. 并使用类似 DT[,shortPath := gsub("(^.*> )","\\1",路径)]。然后检查这个短路径是否出现在你的短路径表中......因此将它与那个合并

标签: r regex optimization substring grepl


【解决方案1】:

给定两个数据集(data.table的形式):

library(data.table) # for data manipulation
library(stringi) # for string manipulation

 >dt1 
                               Path Row   rm
 1:     >root>aaaa>bbbb>cccc>dddd>hello   1 TRUE
 2: >root>aaaa>bbbb>cccc>dddd>greetings   2 TRUE
 3:   >root>aaaa>bbbb>cccc>dddd>example   3 TRUE
 4:      >root>iiii>jjjj>kkkk>llll>mmmm   4 TRUE
 5:   >root>iiii>jjjj>kkkk>nnnn>testing   5 TRUE

 > dt2 # introduced column name `names`

                        names
 1:      >root>aaaa>bbbb>cccc>dddd
 2:      >root>aaaa>bbbb>eeee>ffff
 3:      >root>aaaa>bbbb>gggg>hhhh
 4: >root>iiii>jjjj>kkkk>llll>mmmm
 5:      >root>iiii>jjjj>kkkk>nnnn
 6:           >root>oooo>pppp>qqqq

dt1b<-cbind(t(dt1[,stri_split(Path,fixed=">")]),dt1[,.(Row,rm)])[,V1:=NULL]
dt2b<-data.table(t(dt2[,stri_split(str = names,fixed=">")]))[,V1:=NULL]

 >dt1b
      V2   V3   V4   V5   V6        V7 Row   rm
1: root aaaa bbbb cccc dddd     hello   1 TRUE
2: root aaaa bbbb cccc dddd greetings   2 TRUE
3: root aaaa bbbb cccc dddd   example   3 TRUE
4: root iiii jjjj kkkk llll      mmmm   4 TRUE
5: root iiii jjjj kkkk nnnn   testing   5 TRUE

 >dt2b
      V2   V3   V4   V5   V6   V7
1: root aaaa bbbb cccc dddd      
2: root aaaa bbbb eeee ffff     
3: root aaaa bbbb gggg hhhh     
4: root iiii jjjj kkkk llll mmmm
5: root iiii jjjj kkkk nnnn     
6: root oooo pppp qqqq      root

最后我将dt1b 的每一行与dt2b 的每一行通过:

  sub1<-subset(dt1b, select = grep("^V+", names(dt1b),perl = TRUE,value = TRUE))

创建(包含)所有可能的比较

  l1<-lapply(seq(1:nrow(sub1)),function(x) {l1<-lapply(seq(1:nrow(dt2b)),function(y) {l2<-data.table(t(sub1[x] %in% dt2b[y]));names(l2)<-paste0(dt2b[y]);return(l2)}); names(l1)<-paste(sub1[x],collapse=" ");return(l1)})

部分结果

     l1[1:2]
    [[1]]
    [[1]]$`root aaaa bbbb cccc dddd hello`
       root aaaa bbbb cccc dddd      
    1: TRUE TRUE TRUE TRUE TRUE FALSE

    [[1]]$<NA>
       root aaaa bbbb  eeee  ffff      
    1: TRUE TRUE TRUE FALSE FALSE FALSE

    [[1]]$<NA>
       root aaaa bbbb  gggg  hhhh      
    1: TRUE TRUE TRUE FALSE FALSE FALSE

    [[1]]$<NA>
       root  iiii  jjjj  kkkk  llll  mmmm
    1: TRUE FALSE FALSE FALSE FALSE FALSE

    [[1]]$<NA>
       root  iiii  jjjj  kkkk  nnnn      
    1: TRUE FALSE FALSE FALSE FALSE FALSE

    [[1]]$<NA>
       root  oooo  pppp  qqqq        root
    1: TRUE FALSE FALSE FALSE FALSE FALSE



    [[2]]
    [[2]]$`root aaaa bbbb cccc dddd greetings`
       root aaaa bbbb cccc dddd      
    1: TRUE TRUE TRUE TRUE TRUE FALSE

    [[2]]$<NA>
       root aaaa bbbb  eeee  ffff      
    1: TRUE TRUE TRUE FALSE FALSE FALSE

    [[2]]$<NA>
       root aaaa bbbb  gggg  hhhh      
    1: TRUE TRUE TRUE FALSE FALSE FALSE

    [[2]]$<NA>
       root  iiii  jjjj  kkkk  llll  mmmm
    1: TRUE FALSE FALSE FALSE FALSE FALSE

    [[2]]$<NA>
       root  iiii  jjjj  kkkk  nnnn      
    1: TRUE FALSE FALSE FALSE FALSE FALSE

    [[2]]$<NA>
       root  oooo  pppp  qqqq        root
    1: TRUE FALSE FALSE FALSE FALSE FALSE

所以现在您可以在每行 dt1b 中获得一个 score,例如0/6(甚至不接近),...,5/6(几乎相同),6/6(完全相同)。

想法(编辑)

这是我的想法:

l2<-lapply(seq_along(1:length(l1)),function(x) {
  z=rbindlist(t(l1[[x]][1:nrow(dt2b)]),fill = TRUE)
  z=cbind(z,score=apply(z,1,sum,na.rm=TRUE))
  setorder(z,-score)
  z[,V1:=NULL]
  z<-cbind(t(rep(names(l1[[x]][1]))),z)
  names(z)[1]<-"initialString"
  return(z)
})


   > l2[1:2]
 [[1]]
                     initialString root aaaa bbbb cccc dddd  eeee  ffff  gggg  hhhh  iiii  jjjj  kkkk  llll  mmmm  nnnn score
 1: root aaaa bbbb cccc dddd hello TRUE TRUE TRUE TRUE TRUE    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA     5
 2: root aaaa bbbb cccc dddd hello TRUE TRUE TRUE   NA   NA FALSE FALSE    NA    NA    NA    NA    NA    NA    NA    NA     3
 3: root aaaa bbbb cccc dddd hello TRUE TRUE TRUE   NA   NA    NA    NA FALSE FALSE    NA    NA    NA    NA    NA    NA     3
 4: root aaaa bbbb cccc dddd hello TRUE   NA   NA   NA   NA    NA    NA    NA    NA FALSE FALSE FALSE FALSE FALSE    NA     1
 5: root aaaa bbbb cccc dddd hello TRUE   NA   NA   NA   NA    NA    NA    NA    NA FALSE FALSE FALSE    NA    NA FALSE     1

 [[2]]
                         initialString root aaaa bbbb cccc dddd  eeee  ffff  gggg  hhhh  iiii  jjjj  kkkk  llll  mmmm  nnnn score
 1: root aaaa bbbb cccc dddd greetings TRUE TRUE TRUE TRUE TRUE    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA     5
 2: root aaaa bbbb cccc dddd greetings TRUE TRUE TRUE   NA   NA FALSE FALSE    NA    NA    NA    NA    NA    NA    NA    NA     3
 3: root aaaa bbbb cccc dddd greetings TRUE TRUE TRUE   NA   NA    NA    NA FALSE FALSE    NA    NA    NA    NA    NA    NA     3
 4: root aaaa bbbb cccc dddd greetings TRUE   NA   NA   NA   NA    NA    NA    NA    NA FALSE FALSE FALSE FALSE FALSE    NA     1
 5: root aaaa bbbb cccc dddd greetings TRUE   NA   NA   NA   NA    NA    NA    NA    NA FALSE FALSE FALSE    NA    NA FALSE     1

...或者通过保留最大score 列的行,(这可以通过:return(z) 更改为上面l2 lapply() 中的return(z[score==max(score)]))和rbindlist(t(l2[1:length(l2)]))

                        initialString root aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj kkkk llll mmmm nnnn score
1:     root aaaa bbbb cccc dddd hello TRUE TRUE TRUE TRUE TRUE   NA   NA   NA   NA   NA   NA   NA   NA   NA   NA     5
2: root aaaa bbbb cccc dddd greetings TRUE TRUE TRUE TRUE TRUE   NA   NA   NA   NA   NA   NA   NA   NA   NA   NA     5
3:   root aaaa bbbb cccc dddd example TRUE TRUE TRUE TRUE TRUE   NA   NA   NA   NA   NA   NA   NA   NA   NA   NA     5
4:      root iiii jjjj kkkk llll mmmm TRUE   NA   NA   NA   NA   NA   NA   NA   NA TRUE TRUE TRUE TRUE TRUE   NA     6
5:   root iiii jjjj kkkk nnnn testing TRUE   NA   NA   NA   NA   NA   NA   NA   NA TRUE TRUE TRUE   NA   NA TRUE     5

initialString 列现在包含初始字符串。以下列将其分解为子字符串及其相似度分数

【讨论】:

  • lapply 确实是一个循环,尽管它看起来与forwhile 不同。
【解决方案2】:

以下代码应该可以立即解决您的问题。

library(data.table)
library(stringi)

Pro0 <- data.table(tPro1)

for (i in 1:length(Sub1$Short_path)) {
  Pro0[stri_detect_fixed(Path, Sub1$Short_path[i]), Short_path:=Sub1$Short_path[i]]
}

使用这种方法,我在一秒钟内将 230k 路径名与 14 个较短的路径名关联起来。

这是我用来创建与您的数据集对应的数据集 tPro1 和 Sub1 的代码:

tPro1 <- data.table('Path' = list.files(path = '/usr', full.names = TRUE, recursive = TRUE))
Sub1 <- data.table('Short_path' = list.files(path = '/usr', full.names = TRUE))

【讨论】:

    【解决方案3】:

    sub 如此之小这一事实有助于减少必要的迭代次数。这是一种比您所拥有的更有效的方法,尽管我在这里仍然使用循环。

    首先,设置一些测试数据。使用您指定的相同尺寸:

    set.seed(123)
    
    sub <- sapply( seq_len( 24 ), function(x) {
        paste( sample( c( letters, ">" ),
                       12,
                       replace = TRUE,
                       prob = c( rep( 1, 26 ), 8 ) ),
               collapse = "")
    } )
    head( sub, 3 )
    # [1] "puhyz>lymjbj" "rn>yc>fbyrda" "qsmop>byrv>k"
    

    使用sub 创建tPro1,以便找到合适的子字符串。

    tPro1 <- paste0( sample( sub,
                             5E5,
                             replace = TRUE ),
                     sample( c( ">hello", ">adf", ">;kjadf" ),
                             5E5,
                             replace = TRUE )
    )
    head( tPro1, 3 )
    # [1] "bjwhrj>j>>zj>adf"   "b>>>zpx>fpvg>hello" ">q>hn>ljsllh>adf"  
    

    现在使用 while 循环。迭代sub,在每次迭代中获得尽可能多的匹配。如果我们到达sub 的末尾,或者所有值都已填满,则停止迭代。

    results <- vector( "character", length( tPro1 ) )
    i <- 1L
    system.time(
        while( sum( results == "" ) > 0L && i <= length( sub ) ) {
            results[ grep( sub[i], tPro1 ) ] <- sub[i]
            i <- i + 1L
        }
    )
    #    user  system elapsed 
    #  4.655   0.007   4.661
    

    输出结果。

    output <- data.frame( tPro1 = tPro1, results = results, stringsAsFactors = FALSE )
    head( output, 3 )
    
    #                             tPro1                  results
    # 1 >>ll>ldsjbzzcszcniwm>>em>;kjadf >>ll>ldsjbzzcszcniwm>>em
    # 2 ijka>ca>>>ddpmhilphqlt>c>;kjadf ijka>ca>>>ddpmhilphqlt>c
    # 3 zpnsniwyletn>qzifzjtrjg>>;kjadf zpnsniwyletn>qzifzjtrjg>
    

    所以这不是一个完全矢量化的解决方案,但它确实为您节省了一些时间。对于您正在使用的相同大小的数据集,我们缩短到 4.6 秒。

    编辑:愚蠢的我,我正在使用 sub 几千个值。在将sub 的大小减少到你说的几十个之后,这会更快!

    编辑:使用您展示的数据,您可能需要先创建 tPro1sub 向量:

    tPro1.vec <- tPro1$Path
    sub <- Sub1$Path
    
    results <- vector( "character", length( tPro1.vec ) )
    i <- 1L
    while( sum( results == "" ) > 0L && i <= length( sub ) ) {
        results[ grep( sub[i], tPro1.vec ) ] <- sub[i]
        i <- i + 1L
    }
    

    【讨论】:

    • 探索这个答案,虽然到目前为止我的结果向量被返回为空。不过会追加任何进一步的进展。使用grep( sub[i], tPro1 ),这将返回tPro1 中所有行的向量,其中sub[i] 是一个子字符串?似乎它的执行速度比我预期的要快得多
    • 是的,没错。您应该在此处输入 subtPro1 作为向量,因此对于您的数据,可能需要将它们作为 tPro1$PathSub1$Path
    • 我添加了一个编辑,这可能会有所帮助。我只是在开始循环之前从您的数据中创建向量,以确保它们的结构正确以进行分析。
    • 这正是我所需要的,干杯!我的运行时间是 279.75 秒。我最初的方法是 500k*24 次迭代,但由于我修剪它实际上应该更少。这种方式总是运行 24*500k 次迭代(?),但效率更高,因为它每次迭代都使用更高效的操作?
    【解决方案4】:

    使用模糊匹配,agrepl

    tPro1$Short_path <- Sub1$Path[ apply(sapply(Sub1$Path, function(i) agrepl(i, tPro1$Path)), 1, which) ] 
    
    tPro1
    
    #                                  Path Row   rm                     Short_path
    # 1     >root>aaaa>bbbb>cccc>dddd>hello   1 TRUE      >root>aaaa>bbbb>cccc>dddd
    # 2 >root>aaaa>bbbb>cccc>dddd>greetings   2 TRUE      >root>aaaa>bbbb>cccc>dddd
    # 3   >root>aaaa>bbbb>cccc>dddd>example   3 TRUE      >root>aaaa>bbbb>cccc>dddd
    # 4      >root>iiii>jjjj>kkkk>llll>mmmm   4 TRUE >root>iiii>jjjj>kkkk>llll>mmmm
    # 5   >root>iiii>jjjj>kkkk>nnnn>testing   5 TRUE      >root>iiii>jjjj>kkkk>nnnn
    

    数据

    tPro1  <- read.table(text = "Path                                  Row      rm                                              
    1  >root>aaaa>bbbb>cccc>dddd>hello         1        TRUE
    2  >root>aaaa>bbbb>cccc>dddd>greetings     2        TRUE
    3  >root>aaaa>bbbb>cccc>dddd>example       3        TRUE
    4  >root>iiii>jjjj>kkkk>llll>mmmm          4        TRUE
    5  >root>iiii>jjjj>kkkk>nnnn>testing       5        TRUE",
                         header = TRUE, stringsAsFactors = FALSE)
    
    
    Sub1 <- data.frame(Path = c(">root>aaaa>bbbb>cccc>dddd",
                                ">root>aaaa>bbbb>eeee>ffff",
                                ">root>aaaa>bbbb>gggg>hhhh",
                                ">root>iiii>jjjj>kkkk>llll>mmmm",
                                ">root>iiii>jjjj>kkkk>nnnn",
                                ">root>oooo>pppp>qqqq"),
                       stringsAsFactors = FALSE)
    

    【讨论】:

    • 我的实现可惜花了一个小时才抛出错误*。我仍然无法理解您的函数中的过程: sapply 遍历每个子路径,并将 agrepl 用于整个 tPro1 集合。这将为每个子路径返回 tPro1 中的索引向量,它们是有效路径。 apply 然后对这些向量做一些事情
    • *Error in [.default(xj, i) : invalid subscript type 'list'。仍在调查中
    • @PrunusPersica 错误可能是因为我的解决方案是使用数据框测试的,我们需要更新以匹配 data.tables。关于双重申请,将其分解以查看每个人在做什么。如果还不清楚,请告诉我。
    猜你喜欢
    • 1970-01-01
    • 2012-11-23
    • 1970-01-01
    • 2014-10-12
    • 1970-01-01
    • 1970-01-01
    • 2016-04-19
    • 2011-11-04
    相关资源
    最近更新 更多