【问题标题】:Increase performance by moving away from a for loop通过远离 for 循环来提高性能
【发布时间】:2016-02-23 14:04:37
【问题描述】:

论证的要点如下:

我写的一个函数考虑了一个参数,一个字母数字字符串,并且应该输出一个字符串,其中这个字母数字字符串的每个元素的值都被切换为一些“映射”。 MRE如下:

#This is the original and switches value map
map = data.table(mapped = c(0:35), original = c(0:9,LETTERS))
#the function that I'm using:
as_numbers <- function(string) {
  #split string unlisted
  vector_unlisted <- unlist(strsplit(string,""))
  #match the string in vector
  for (i in 1:length(vector_unlisted)) {

    vector_unlisted[i] <- subset(map, map$original==vector_unlisted[i])[[1]][1]

  }
  vector_unlisted <- paste0(vector_unlisted, collapse = "")

  return(vector_unlisted)
}

我正试图从 for loop 移开一些可以提高性能的东西,因为该功能可以工作,但是对于我以这种形式提供的元素数量来说它非常慢:

unlist(lapply(dat$alphanum, function(x) as_numbers(x)))

输入字符串的示例是:549300JV8KEETQJYUG13。这应该会产生一个类似5493001931820141429261934301613

的字符串

在这种情况下只提供一个字符串:

> as_numbers("549300JV8KEETQJYUG13")
[1] "5493001931820141429261934301613"

【问题讨论】:

标签: r data.table


【解决方案1】:

我们可以使用基础转换:

#input and expected output
x <- "549300JV8KEETQJYUG13"
# "5493001931820141429261934301613"

#output
res <- paste0(strtoi(unlist(strsplit(x, "")), base = 36), collapse = "")

#test output
as_numbers(x) == res
# [1] TRUE

性能

由于这篇文章是关于性能的,这里是针对 3 个解决方案的基准测试*:

#input set up
map = data.table(mapped = c(0:35), original = c(0:9,LETTERS))
x <- rep(c("549300JV8KEETQJYUG13", "5493V8KE300J"), 1000)

#define functions
base_f <- function(string) {
  sapply(string, function(x) {
    paste0(strtoi(unlist(strsplit(x, "")), base = 36), collapse = "")
    })
  }

match_f <- function(string) {
  mapped <- map$mapped
  original <- map$original
  sapply(strsplit(string, ""), function(y) {
    paste0(mapped[match(y, original)], collapse= "")})
  }

reduce_f <- function(string) {
  Reduce(function(string,r) 
    gsub(map$original[r],
         map$mapped[r], string, fixed = TRUE),
    seq_len(nrow(map)), string)
  }

#test if all return same output
all(base_f(x) == match_f(x))
# [1] TRUE
all(base_f(x) == reduce_f(x))
# [1] TRUE

library(rbenchmark)
benchmark(replications = 1000,
          base_f(x),
          match_f(x),
          reduce_f(x))
#          test replications elapsed relative user.self sys.self user.child sys.child
# 1   base_f(x)         1000   22.15    4.683     22.12        0         NA        NA
# 2  match_f(x)         1000   19.18    4.055     19.11        0         NA        NA
# 3 reduce_f(x)         1000    4.73    1.000      4.72        0         NA        NA

*注意:microbenchmark() 不断抛出警告,因此使用 rbenchmark() 代替。随意使用其他库进行测试并更新这篇文章。

【讨论】:

    【解决方案2】:

    使用Reducegsub,你可以定义如下函数

    replacer <- function(x) Reduce(function(x,r) gsub(map$original[r],
                 map$mapped[r], x, fixed=T), seq_len(nrow(map)),x)
    
    
    # Let's test it
    replacer("549300JV8KEETQJYUG13")
    #[1] "5493001931820141429261934301613"
    

    【讨论】:

      【解决方案3】:

      似乎是合并:

      map[as.data.table(unlist(strsplit(string, ""))),
          .(mapped), on = c(original = "V1")][ , paste0(mapped, collapse = "")]
      

      注意“D1”和“1V”都会映射到“131”...

      在您的示例输出中是:"5493001931820141429261934301613"

      如果您真的希望这是一个可逆映射,您可以使用sep = "."...

      【讨论】:

      • 是的,崩溃是必须的。在这种情况下,它是标准的一部分
      • @erasmortg 为什么不使用“00”到“35”?
      • 它是 ISO 7064 标准的一部分。它从 0 = 0 开始直到 z = 36
      • @erasmortg 分隔符怎么样...例如"D1" -> "13.1", "1V" -> "1.31"...
      • 我必须在某一时刻去掉分隔符,输出字符串应该是一个整数
      【解决方案4】:

      我会使用match:

      as_numbers <- function(string) {
        lapply(strsplit(string, ""), function(y) {
          paste0(map$mapped[match(y, map$original)], collapse= "")})
      }
      
      as_numbers(c("549300JV8KEETQJYUG13", "5493V8KE300J"))
      #[[1]]
      #[1] "5493001931820141429261934301613"
      #
      #[[2]]
      #[1] "5493318201430019"
      

      添加了一个 lapply 调用以正确处理长度 > 1 的输入。

      如果您需要进一步加快速度,您可以将map$mappedmap$original 存储在单独的向量中,并在match 调用中使用它们而不是map$...,这样您就不需要对data.frame/ 进行子集化data.table 这么多次(这是相当昂贵的)。


      由于 Q 是关于性能的,这里是两个解决方案的基准:

      map = data.table(mapped = c(0:35), original = c(0:9,LETTERS))
      x <- rep(c("549300JV8KEETQJYUG13", "5493V8KE300J"), 1000)
      
      ascii_func <- function(string) {
        lapply(string, function(x) {
          x_ascii <- strtoi(charToRaw(x), 16)
          paste(ifelse(x_ascii >= 65 & x_ascii <= 90,
                        x_ascii - 55, x_ascii - 48),
                        collapse = "")
        })
      }
      
      match_func <- function(string) {
        mapped <- map$mapped
        original <- map$original
          lapply(strsplit(string, ""), function(y) {
            paste0(mapped[match(y, original)], collapse= "")})
      }
      
      library(microbenchmark)
      microbenchmark(ascii_func(x), match_func(x), times = 25L)
      #Unit: milliseconds
      #          expr   min    lq  mean median     uq    max neval
      # ascii_func(x) 83.47 92.55 96.91  96.82 103.06 112.07    25
      # match_func(x) 24.30 24.74 26.86  26.11  28.67  31.55    25
      
      identical(ascii_func(x), match_func(x))
      #[1] TRUE
      

      【讨论】:

      • 您可能希望在基准测试中也包含@mtoto 的解决方案。在我的电脑上,这是最快的。
      猜你喜欢
      • 2018-11-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-05
      • 1970-01-01
      相关资源
      最近更新 更多