【问题标题】:split string into non-overlapping segments将字符串拆分为不重叠的段
【发布时间】:2016-05-20 01:52:53
【问题描述】:

我希望将字符串拆分为不重叠的段,其中段的端点是点域内的数字。我可以使用下面的代码来做到这一点。但是,这段代码似乎过于复杂,并且涉及到嵌套的for-loops。有没有更简单的方法,最好在基础R 中使用regex?

这是一个示例和desired.result。

my.data <- read.table(text = '
     my.string   cov1  cov2
     11.......     1     A
     1.1.2.1.1     2     B
     1234.....     3     C
     1...2...3     4     C
     ..3..4...     5     D
', header = TRUE, stringsAsFactors = FALSE)

desired.result <- read.table(text = '
     my.string    cov1     cov2
     11.......      1        A
     1.1......      2        B
     ..1.2....      2        B
     ....2.1..      2        B
     ......1.1      2        B
     12.......      3        C
     .23......      3        C
     ..34.....      3        C
     1...2....      4        C
     ....2...3      4        C
     ..3..4...      5        D
', header = TRUE, stringsAsFactors = FALSE, na.strings = 'NA')


new.data <- data.frame(do.call(rbind, strsplit(my.data$my.string,'')), stringsAsFactors = FALSE)

n.segments <- rowSums(!(new.data[1:ncol(new.data)] == '.')) - 1

my.end.points <- do.call(rbind, gregexpr("[0-9]", my.data$my.string, perl=TRUE))

my.end.point.char <- do.call(rbind, strsplit(my.data$my.string, ""))

my.end.point.char <- t(apply(my.end.point.char, 1, as.numeric))

new.strings <- matrix('.', nrow = sum(n.segments), ncol = max(nchar(my.data$my.string)))

new.cov     <- as.data.frame(matrix(NA,  nrow = sum(n.segments), ncol = (ncol(my.data) - 1)))

m <- 1

for(i in 1:nrow(new.data)) {
     for(j in 1:n.segments[i]) {
          for(k in 1:ncol(new.strings)) {

               new.strings[m, my.end.points[i,  j   ] ] <- my.end.point.char[i, my.end.points[i, j   ]]
               new.strings[m, my.end.points[i, (j+1)] ] <- my.end.point.char[i, my.end.points[i,(j+1)]]
               new.cov[m,] <- my.data[i, c(2:ncol(my.data))]

          }
          m <- m + 1
     }
}


my.result <- data.frame(my.string = apply(new.strings, 1, function(x) paste0(x, collapse = '')), stringsAsFactors = FALSE)
my.result <- data.frame(my.result, new.cov)
colnames(my.result) <- names(my.data)

all.equal(desired.result, my.result)

# [1] TRUE

【问题讨论】:

  • 艰难的一个....我把它计算为一行如下:s &lt;- '1.1.2.1.1'; s &lt;- unlist(strsplit(s, '*')); t(apply(combn(which(s != '.'), 2), 2, function(x) {y &lt;- rep('.', 9); y[x] &lt;- s[x]; y})); 还需要更多apply 家庭功能。如果有人没有发布更好的答案,明天再看看。
  • 也许,根据您的目标,将my.data$mystring 设置为with(list(gr = gregexpr("[[:digit:]]", my.data$my.string)), Map(function(n, i, x) replace(integer(n), i, as.numeric(x)), nchar(my.data$my.string), gr, regmatches(my.data$my.string, gr))) 或其他非字符对象可能更方便?

标签: regex r string


【解决方案1】:
w <- nchar(my.data$my.string[1L]);
dps <- character(w+1L); dps[1L] <- ''; for (i in seq_len(w)) dps[i+1L] <- paste0(dps[i],'.');
x <- Map(my.data$my.string,gregexpr('[^.]',my.data$my.string),f=function(s,g)
    if (length(g)<3L) s else sapply(seq_len(length(g)-1L),function(gi)
        paste0(dps[g[gi]],substr(s,g[gi],g[gi+1L]),dps[w-g[gi+1L]+1L])
    )
);
res <- transform(my.data[rep(seq_len(nrow(my.data)),sapply(x,length)),],my.string=unlist(x));
res;
##     my.string cov1 cov2
## 1   11.......    1    A
## 2   1.1......    2    B
## 2.1 ..1.2....    2    B
## 2.2 ....2.1..    2    B
## 2.3 ......1.1    2    B
## 3   12.......    3    C
## 3.1 .23......    3    C
## 3.2 ..34.....    3    C
## 4   1...2....    4    C
## 4.1 ....2...3    4    C
## 5   ..3..4...    5    D

注意:如果您有足够新的 R 版本,您可以将 sapply(x,length) 替换为 lengths(x)。


基准测试

library(microbenchmark);

bgoldst <- function(my.data) { w <- nchar(my.data$my.string[1L]); dps <- character(w+1L); dps[1L] <- ''; for (i in seq_len(w)) dps[i+1L] <- paste0(dps[i],'.'); x <- Map(my.data$my.string,gregexpr('[^.]',my.data$my.string),f=function(s,g) if (length(g)<3L) s else sapply(seq_len(length(g)-1L),function(gi) paste0(dps[g[gi]],substr(s,g[gi],g[gi+1L]),dps[w-g[gi+1L]+1L]))); transform(my.data[rep(seq_len(nrow(my.data)),sapply(x,length)),],my.string=unlist(x)); };
rawr <- function(my.data) { f <- function(x, m) { y <- gsub('.', '\\.', x); cs <- attr(m, "capture.start"); cl <- attr(m, "capture.length"); Vectorize(`substr<-`)(y, cs, cl + cs - 1, Vectorize(substr)(x, cs, cl + cs - 1)); }; m <- gregexpr('(?=([0-9][.]*[0-9]))', my.data$my.string, perl = TRUE); strs <- Map(f, my.data$my.string, m); tmp <- `rownames<-`(my.data[rep(1:nrow(my.data), sapply(strs,length)), ], NULL); tmp$my.string <- unlist(strs); tmp; };
carroll <- function(my.data) { strings <- sapply(my.data$my.string, function(x) { stri_match_all_regex(x, "(?=([0-9]{1}\\.*[0-9]{1}))")[[1]][,2]; }); strpos <- lapply(1:length(strings), function(x) { y <- {nchar(sub(perl=T,'^\\.*\\K.*','',my.data$my.string[x]))+c(0, cumsum(nchar(strings[[x]])-1))}; return(y[-length(y)]); }); w <- nchar(my.data$my.string[1L]); output.result <- data.frame(my.string = cbind(unlist(sapply(1:length(strings), function(y) { cbind(sapply(1:length(strings[[y]]), function(x) { leftstr  <- paste0(paste0(rep(".", strpos[[y]][[x]]), collapse=""), strings[[y]][x]); rightstr <- paste0(rep(".", w-nchar(leftstr)), collapse=""); paste0(leftstr, rightstr, collapse=""); })); }))), my.data[unlist(sapply(1:length(strings), function(x) { rep(x, sapply(strings, length)[x]); })), c(2,3)], stringsAsFactors=FALSE); row.names(output.result) <- NULL; output.result; };

## OP's sample input
my.data <- read.table(text = '
     my.string   cov1  cov2
     11.......     1     A
     1.1.2.1.1     2     B
     1234.....     3     C
     1...2...3     4     C
     ..3..4...     5     D
', header = TRUE, stringsAsFactors = FALSE);

ex <- bgoldst(my.data);
all.equal(ex,rawr(my.data),check.attributes=F);
## [1] TRUE
all.equal(ex,carroll(my.data),check.attributes=F);
## [1] TRUE

microbenchmark(bgoldst(my.data),rawr(my.data),carroll(my.data));
## Unit: microseconds
##              expr      min       lq      mean    median       uq      max neval
##  bgoldst(my.data)  422.094  451.816  483.5305  476.6195  503.775  801.421   100
##     rawr(my.data) 1096.502 1160.863 1277.7457 1236.7720 1298.996 3092.785   100
##  carroll(my.data) 1130.287 1176.900 1224.6911 1213.2515 1247.249 1525.437   100

## scale test
set.seed(1L);
NR <- 1e4; NS <- 30L; probDot <- 3/4;
x <- c('.',0:9); probs <- c(probDot,rep((1-probDot)/10,10L)); my.data <- data.frame(my.string=do.call(paste0,as.data.frame(replicate(NS,sample(x,NR,T,probs)))),cov1=sample(seq_len(NR)),cov2=sample(make.unique(rep(LETTERS,len=NR))),stringsAsFactors=F);
repeat { w <- which(sapply(gregexpr('[^.]',my.data$my.string),length)==1L); if (length(w)==0L) break; my.data$my.string[w] <- do.call(paste0,as.data.frame(replicate(NS,sample(x,length(w),T,probs)))); }; ## prevent single-digit strings, which rawr and carroll solutions don't support

ex <- bgoldst(my.data);
all.equal(ex,rawr(my.data),check.attributes=F);
## [1] TRUE
all.equal(ex,carroll(my.data),check.attributes=F);
## [1] TRUE

microbenchmark(bgoldst(my.data),rawr(my.data),carroll(my.data),times=1L);
## Unit: milliseconds
##              expr        min         lq       mean     median         uq        max neval
##  bgoldst(my.data)    904.887    904.887    904.887    904.887    904.887    904.887     1
##     rawr(my.data)   2736.462   2736.462   2736.462   2736.462   2736.462   2736.462     1
##  carroll(my.data) 108575.001 108575.001 108575.001 108575.001 108575.001 108575.001     1

【讨论】:

    【解决方案2】:
    my.data <- read.table(text = '
         my.string   cov1  cov2
                          11.......     1     A
                          1.1.2.1.1     2     B
                          1234.....     3     C
                          1...2...3     4     C
                          ..3..4...     5     D
                          ', header = TRUE, stringsAsFactors = FALSE)
    
    f <- function(x, m) {
      if (nchar(gsub('.', '', x, fixed = TRUE)) < 2L) return(x)
      y <- gsub('.', '\\.', x)
      cs <- attr(m, "capture.start")
      cl <- attr(m, "capture.length")
      Vectorize(`substr<-`)(y, cs, cl + cs - 1, Vectorize(substr)(x, cs, cl + cs - 1))
    }
    
    m <- gregexpr('(?=([0-9][.]*[0-9]))', my.data$my.string, perl = TRUE)
    strs <- Map(f, my.data$my.string, m)
    
    tmp <- `rownames<-`(my.data[rep(1:nrow(my.data), lengths(strs)), ], NULL)
    tmp$my.string <- unlist(strs)
    
    #    my.string cov1 cov2
    # 1  11.......    1    A
    # 2  1.1......    2    B
    # 3  ..1.2....    2    B
    # 4  ....2.1..    2    B
    # 5  ......1.1    2    B
    # 6  12.......    3    C
    # 7  .23......    3    C
    # 8  ..34.....    3    C
    # 9  1...2....    4    C
    # 10 ....2...3    4    C
    # 11 ..3..4...    5    D
    
    identical(tmp, desired.result)
    # [1] TRUE
    

    【讨论】:

    • 不错的解决方案。但是,如果输入中的字符串在点域中恰好有一个数字,那么这个解决方案目前不能正确处理这种情况;它丢失了数字。 (OP 没有说明这种情况是否会发生,但为了稳健性,我认为保留这些情况的数字会很好。)
    • 例如,试试这个随机输入:set.seed(3L); NR &lt;- 5L; NS &lt;- 9L; probDot &lt;- 3/4; x &lt;- c('.',0:9); probs &lt;- c(probDot,rep((1-probDot)/10,10L)); my.data &lt;- data.frame(my.string=do.call(paste0,as.data.frame(replicate(NS,sample(x,NR,T,probs)))),cov1=sample(seq_len(NR)),cov2=sample(make.unique(rep(LETTERS,len=NR))),stringsAsFactors=F);。您的解决方案目前在第三和第四个字符串中丢失了 7 和 2。
    • @bgoldst 谢谢,我明白你的意思。我确信正则表达式可以改进并处理所有情况,这高于我的工资等级,但由于这是一个特殊情况,我现在只是添加了一行来覆盖它
    【解决方案3】:

    这是一个选项。不干净,但也不是问题。

    library(stringi)
    
    ## isolate the strings, allowing overlap via positive lookaheads
    strings <- sapply(my.data$my.string, function(x) {
      stri_match_all_regex(x, "(?=([0-9]{1}\\.*[0-9]{1}))")[[1]][,2]
    })
    

    确定每组开始处的偏移量。

    ## identify the . offsets
    strpos <- lapply(1:length(strings), function(x) {
      y <- {nchar(sub(perl=T,'^\\.*\\K.*','',my.data$my.string[x]))+c(0, cumsum(nchar(strings[[x]])-1))}
      return(y[-length(y)])
    })
    

    仅使用 2 个 sapply 循环构建 data.frame。

    ## collate the results using sapply
    w <- nchar(my.data$my.string[1L]);
    output.result <- data.frame(
      my.string = cbind(unlist(sapply(1:length(strings), function(y) { 
        cbind(sapply(1:length(strings[[y]]), function(x) {
          leftstr  <- paste0(paste0(rep(".", strpos[[y]][[x]]), collapse=""), strings[[y]][x])
          rightstr <- paste0(rep(".", w-nchar(leftstr)), collapse="")
          paste0(leftstr, rightstr, collapse="")
        }))
      }))), 
      my.data[unlist(sapply(1:length(strings), function(x) {
        rep(x, sapply(strings, length)[x])
      })), c(2,3)], stringsAsFactors=FALSE
    )
    row.names(output.result) <- NULL
    output.result
    
       my.string cov1 cov2
    1  11.......    1    A
    2  1.1......    2    B
    3  ..1.2....    2    B
    4  ....2.1..    2    B
    5  ......1.1    2    B
    6  12.......    3    C
    7  .23......    3    C
    8  ..34.....    3    C
    9  1...2....    4    C
    10 ....2...3    4    C
    11 ..3..4...    5    D
    
    identical(desired.result, output.result)
    [1] TRUE
    

    【讨论】:

    • 最后一个字符串不正确;应该是..3..4...。
    • 很好,我不确定那条线的逻辑并做了一个假设。答案已更新。
    • 很好,问题已解决。虽然我刚刚发现了第二个问题,那就是在字符串中只有一个数字的情况下你会得到 NA 。 @rawr 的解决方案也没有正确理解这种情况,我也在他的回答下对此发表了评论。试试这个输入:set.seed(3L); NR &lt;- 5L; NS &lt;- 9L; probDot &lt;- 3/4; x &lt;- c('.',0:9); probs &lt;- c(probDot,rep((1-probDot)/10,10L)); my.data &lt;- data.frame(my.string=do.call(paste0,as.data.frame(replicate(NS,sample(x,NR,T,probs)))),cov1=sample(seq_len(NR)),cov2=sample(make.unique(rep(LETTERS,len=NR))),stringsAsFactors=F);.
    • OP 说“endpoints”所以我认为单个数字超出规范。
    • 是的,我认为这是一个合理的论点。但我认为最好支持这种情况,因为我们不知道 OP 在他的应用程序中可能必须处理哪些类型的输入。在软件的世界里,几乎总是没有充分地规定需求,如果它们被指定的话,那么尝试编写尽可能全面的解决方案符合每个人的最大利益。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-11
    • 1970-01-01
    • 2014-06-24
    • 1970-01-01
    • 1970-01-01
    • 2011-01-27
    • 2016-07-31
    相关资源
    最近更新 更多