首页 > 解决方案 > 在 data.table 中包含迭代循环的矢量化函数

问题描述

我有一个数据集,我需要在其中迭代地根据它们的结构规律提取邮政编码。我首先需要检测“AA00 0AA”,然后才能检测到“A00 0AA”,因为如果我还没有排除它,第二个也会检测到第一个。我不是在寻找解决方法(比如通过在正则表达式之前添加一个空格或类似的东西来改进正则表达式),我试图直观地理解以下问题,因为我的工作中出现了更多具有类似问题的函数:

数据:

library("data.table")
library("stringr")
testdata <- data.table(Address = c("1 Some Street, sometown, AA00 0AA",
                                   "1 Some Street, sometown, A00 0AA"))

我的功能如下所示:

my_postcode_fun <- function(myscalar){
 allpatterns <- c("[[:alpha:]][[:alpha:]][[:digit:]][[:digit:]][[:space:]][[:digit:]][[:alpha:]][[:alpha:]]", 
                   # this is AA00 0AA
                  "[[:alpha:]][[:digit:]][[:digit:]][[:space:]][[:digit:]][[:alpha:]][[:alpha:]]" 
                   # this is A00 0AA
                  )# these are the patterns I'm looking for

 for(i in allpatterns){
 if(!is.na(str_extract(myscalar, regex(i))[1])){ # only run the next steps, if found at least once
  
     output <- list(postcode = str_extract(myscalar, regex(i)),     # extract the postcode
               leftover = str_replace(myscalar, regex(i), ""))      # extract old string without the postcode
     break # stop if you've found one
 }
 }
 return(output) # return both elements
}

此函数适用于标量:

my_postcode_fun(testdata[1])

我可以通过一个向量循环它:

for(i in 1:2){
  print(my_postcode_fun(testdata[i,]))
}

当我强制 data.table 逐行运行时,它也有效:

new <- testdata[, c("postcode", "Address"):= 
                  my_postcode_fun(Address), 
                  by = seq_len(nrow(testdata))]
new

但这是非常低效的。我不能让这个函数在 data.table 中使用它的“同时对每个 i 完成所有事情”逻辑。

testdata[, c("postcode", "Address"):= 
           my_postcode_fun(Address)]

我认为有两个问题,如果我错了,请纠正我。

第一个问题是 data.table 对每一行执行整个操作,然后进入循环的下一步。您可以通过注释掉函数中的“break”来看到这一点,在这种情况下,它只返回循环的第二步(并且可能已经覆盖了第一步)。

我认为的第二个问题是“中断”将停止所有行的循环。至少这是我对 data.table 如何处理循环的直觉。如果它没有找到特定行的任何内容,我希望它继续。所有元素的第一步,然后是所有元素的第二步等。

那么,我怎样才能对它进行矢量化并在 data.table 中使用它并有一个迭代(!)循环,该循环需要在允许第二步发生之前发生第一步?我试图让这个例子更简单,但我害怕错过重要的部分,因为我不完全理解它。

标签: rdata.table

解决方案


首先,if条件需要单个逻辑,而不是向量,如果myscalar长度超过 1(或仅 0),那么它将失败。此外,从if某种意义上说,应该确实具有矢量化比较,因为您可能有一个模式匹配一​​个但不是全部,等等。

为此,人们可能会认为ifelse这是一种矢量化if/else替代方法,但我认为另一种方法是一种归约方法,在这种方法中,在任何尚未匹配的输入上尝试每个模式,并在一切都匹配时停止处理。


my_postcode_fun <- function(myvector){
  allpatterns <- c("[[:alpha:]][[:alpha:]][[:digit:]][[:digit:]][[:space:]][[:digit:]][[:alpha:]][[:alpha:]]", 
                   # this is AA00 0AA
                   "[[:alpha:]][[:digit:]][[:digit:]][[:space:]][[:digit:]][[:alpha:]][[:alpha:]]" 
                   # this is A00 0AA
                   )# these are the patterns I'm looking for

  post <- rep(NA_character_, length(myvector))
  
  for (ptn in allpatterns) {
    isna <- is.na(post)
    if (!any(isna)) break
    post[isna] <- str_extract(myvector[isna], regex(ptn))
  }

  isna <- is.na(post)
  if (any(!isna)) {
    myvector[!isna] <- mapply(sub, post[!isna], "", myvector[!isna])
  }
  list(postcode = post, leftover = myvector)
}

testdata <- data.table(Address = c("1 Some Street, sometown, AA00 0AA",
                                   "1 Some Street, sometown, A00 0AA",
                                   "1 Some Street, sometown, "))

testdata[, c("post","left") := my_postcode_fun(Address)][]
#                              Address     post                      left
#                               <char>   <char>                    <char>
# 1: 1 Some Street, sometown, AA00 0AA AA00 0AA 1 Some Street, sometown, 
# 2:  1 Some Street, sometown, A00 0AA  A00 0AA 1 Some Street, sometown, 
# 3:         1 Some Street, sometown,      <NA> 1 Some Street, sometown, 

postandleft字符串肯定可以被清理掉,也许trimws,但这是一项不同的任务。)

快速演练:

  • 我们首先生成一个post与 input 一样长的向量myscalar,但 all NA,这是一个有意的起点。
  • 对于ptn模式中的每一个,首先我们只想查看那些postNA(是的,第一遍总是全真,但是这个状态机的重点是它不需要知道它是否在第一次或n第一次通过)。为此,我们定义isna了一个变量来跟踪哪些postNA,这意味着哪些myscalar还没有匹配。
  • 快速检查:如果所有内容都匹配(即!any(isna)),则停止处理。这是一个很好的断点,因为这意味着如果我们有 1M 输入并且第一个模式匹配所有这些,那么我们不必继续处理任何其他模式。
  • str_extract剩余的输入与 this ptn,并存储在尚未匹配的post输出中。
  • for循环之后,我们再填充isna一次,以便我们可以从原始输入中删除邮政编码。sub没有对其模式进行矢量化(仅在其输入x=矢量上),因此我mapply在某种意义上使用对其进行矢量化(还有其他方法可以这样做)。
    • 注意:这里有一个非常小的风险:如果输入中有两个明显的邮政编码(尽管不太可能),并且它们包含相同的匹配子字符串,那么第一个将被提取到post中,sub并将删除第一个匹配项。这些步骤都不会承认或删除第二个。我怀疑这可能,但我想确定这种可能性。
  • 顺便说一句:虽然我用postcodeand leftover(如您的问题)命名了函数输出,但在这种情况下,这些名称被 inside 删除c("post","left") :=data.table。我以不同的方式命名它们来证明这一点。该函数可以很容易地返回并且它 list(post, leftover)可以正常工作(尽管我认为名称是好的,声明性的,如果这个函数曾经在 之外使用data.table,这些名称可能会很有帮助)。

推荐阅读