结合使用pmap和a将不同的正则表达式应用于小标题中的不同变量?

时间:2018-10-30 19:28:35

标签: r purrr

这个问题与Using pmap to apply different regular expressions to different variables in a tibble?非常相似,但是有所不同,因为我意识到我的示例不足以描述我的问题。

我正在尝试将不同的正则表达式应用于不同的变量。例如,我用小标题列出了1)我要修改的变量名,2)我要匹配的正则表达式,3)替换字符串。我想将正则表达式/替换应用于其他数据框中的变量。请注意,目标小节中可能有一些变量我不想修改,并且“配置”小节中的行顺序可能与“目标”小节中的列/变量顺序不符。

所以我的“配置”提示看起来像这样:

test_config <-  dplyr::tibble(
  string_col = c("col1", "col2", "col4", "col3"),
  pattern = c("^\\.$", "^NA$", "^$", "^NULL$"),
  replacement = c("","","", "")
)

我想将其应用于目标小标题:

test_target <- dplyr::tibble(
  col1 = c("Foo", "bar", ".", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "NA", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", "NULL"),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

所以目标是用用户指定的test_target列/变量中的空字符串替换另一个字符串。

结果应该是这样的:

result <- dplyr::tibble(
  col1 = c("Foo", "bar", "", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", ""),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

我可以使用for循环来完成我想做的事情,像这样:

for (i in seq(nrow(test_config))) {
  test_target <- dplyr::mutate_at(test_target,
                   .vars = dplyr::vars(
                     tidyselect::matches(test_config$string_col[[i]])),
                   .funs = dplyr::funs(
                     stringr::str_replace_all(
                       ., test_config$pattern[[i]], 
                       test_config$replacement[[i]]))
  )
}

相反,是否有更整洁的方式来做我想要的事情? 到目前为止,我认为purrr::pmap是完成这项工作的工具,所以我制作了一个函数,该函数接受一个数据框,变量名,正则表达式和替换值,并在修改了单个变量的情况下返回该数据框。它的行为符合预期:

testFun <- function(df, colName, regex, repVal){
  colName <- dplyr::enquo(colName)
  df <- dplyr::mutate_at(df,
                         .vars = dplyr::vars(
                           tidyselect::matches(!!colName)),
                         .funs = dplyr::funs(
                           stringr::str_replace_all(., regex, repVal))
  )
}

# try with example
out <- testFun(test_target, 
               test_config$string_col[[1]], 
               test_config$pattern[[1]], 
               "")

但是,当我尝试在pmap中使用该功能时,遇到了两个问题: 1)有比这更好的方法来构建pmap调用的列表吗?

purrr::pmap(
    list(test_target, 
         test_config$string_col, 
         test_config$pattern, 
         test_config$replacement),
    testFun
)

2)致电pmap时出现错误:

Error: Element 2 has length 4, not 1 or 5.

因此,pmap对尝试将长度为5的小标题作为其他元素长度为4的列表的元素进行传递感到不满意(我认为这将回收该小标题)。

还请注意,以前,当我以四行小调呼叫pmap时,我遇到了另一个错误,

Error in UseMethod("tbl_vars") : 
  no applicable method for 'tbl_vars' applied to an object of class "character"
Called from: tbl_vars(tbl)

你们中的任何人都可以建议使用pmap来完成我想做的事情吗,还是有其他解决方法或更好的方法来解决问题?

谢谢!

4 个答案:

答案 0 :(得分:2)

这里有两种tidyverse方式。一种类似于data.table的答案,因为它涉及到重塑数据,将其与配置结合在一起,然后重塑回宽范围。另一种是基于purrr的方法,在我看来,这有点奇怪。我建议使用第一种,因为它感觉更直观。

使用tidyr::gather来使数据变长,然后使用dplyr::left_join确保test_target中的每个文本值都具有对应的模式和替换-即使情况(col5)不模式将通过左连接保留。

library(tidyverse)
...

test_target %>%
  gather(key = col, value = text) %>%
  left_join(test_config, by = c("col" = "string_col"))
#> # A tibble: 25 x 4
#>    col   text  pattern replacement
#>    <chr> <chr> <chr>   <chr>      
#>  1 col1  Foo   "^\\.$" ""         
#>  2 col1  bar   "^\\.$" ""         
#>  3 col1  .     "^\\.$" ""         
#>  4 col1  NA    "^\\.$" ""         
#>  5 col1  NULL  "^\\.$" ""         
#>  6 col2  Foo   ^NA$    ""         
#>  7 col2  bar   ^NA$    ""         
#>  8 col2  .     ^NA$    ""         
#>  9 col2  NA    ^NA$    ""         
#> 10 col2  NULL  ^NA$    ""         
#> # ... with 15 more rows

使用ifelse替换存在模式的模式,如果模式不存在,则保留原始文本。保留必要的模式,添加行号,因为spread需要唯一的ID,然后再次使数据变宽。

test_target %>%
  gather(key = col, value = text) %>%
  left_join(test_config, by = c("col" = "string_col")) %>% 
  mutate(new_text = ifelse(is.na(pattern), text, str_replace(text, pattern, replacement))) %>%
  select(col, new_text) %>%
  group_by(col) %>%
  mutate(row = row_number()) %>%
  spread(key = col, value = new_text) %>%
  select(-row)
#> # A tibble: 5 x 5
#>   col1  col2  col3  col4  col5    
#>   <chr> <chr> <chr> <chr> <chr>   
#> 1 Foo   Foo   Foo   NULL  I       
#> 2 bar   bar   bar   NA    am      
#> 3 ""    .     .     Foo   not     
#> 4 NA    ""    NA    .     changing
#> 5 NULL  NULL  ""    bar   .

第二种方法是仅对列名进行细微的排列,将其与配置结合在一起,然后拆分为列表列表。然后purrr::map2_dfc映射到您创建的该列表和test_target的列,并通过cbind ing返回数据帧。起作用的原因是,数据框从技术上讲是列的列表,因此,如果在数据框上进行映射,则将每一列都视为列表项。我无法在这里工作ifelse-逻辑中只有一个字符串返回,而不是整个向量。

tibble(all_cols = names(test_target)) %>%
  left_join(test_config, by = c("all_cols" = "string_col")) %>%
  split(.$all_cols) %>%
  map(as.list) %>%
  map2_dfc(test_target, function(info, text) {
    if (is.na(info$pattern)) {
      text
    } else {
      str_replace(text, info$pattern, info$replacement)
    }
  })
#> # A tibble: 5 x 5
#>   col1  col2  col3  col4  col5    
#>   <chr> <chr> <chr> <chr> <chr>   
#> 1 Foo   Foo   Foo   NULL  I       
#> 2 bar   bar   bar   NA    am      
#> 3 ""    .     .     Foo   not     
#> 4 NA    ""    NA    .     changing
#> 5 NULL  NULL  ""    bar   .

reprex package(v0.2.1)于2018-10-30创建

答案 1 :(得分:1)

我对purrrdplyr并没有经验,但是这里是data.table的一种使用方法。可以使用一些谷歌搜索方法将其移到dplyr中:)

就可解释性而言,使用循环的方法可以说更好,因为它更简单。

编辑:对代码进行了一些更改,最终没有使用purrr

# alternative with data.table
library(data.table)
library(dplyr)

# objects
test_config <-  dplyr::tibble(
  string_col = c("col1", "col2", "col4", "col3"),
  pattern = c("^\\.$", "^NA$", "^$", "^NULL$"),
  replacement = c("","","", "")
)
test_target <- dplyr::tibble(
  col1 = c("Foo", "bar", ".", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "NA", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", "NULL"),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

multiColStringReplace <- function(test_target, test_config){

  # data.table conversion
  test_target <- as.data.table(test_target)
  test_config <- as.data.table(test_config)

  # adding an id column, as I'm reshaping the data, helps for identification of rows
  # throughout the process
  test_target[,id:=1:.N]

  # wide to long format
  test_target2 <- melt(test_target, id.vars="id")
  head(test_target2)

  # pull in the configuration, can join up on one column now
  test_target2 <- merge(test_target2, test_config, by.x="variable",
                        by.y="string_col", all.x=TRUE)

  # this bit still looks messy to me, haven't used pmap before.
  # I've had to subset the data to the required format, run the pmap with gsub
  # to complete the task, then assign the unlisted vector back in to the original
  # data. Would like to see a better option too!
  test_target2[, result := value]
  test_target2[!is.na(pattern), result := gsub(pattern, replacement, value),
               by = .(id, variable)]

  # case from long to original format, and drop the id
  output <- dcast(test_target2, id~variable,
                  value.var = "result")
  output[, id := NULL]

  # back to tibble
  output <- as_tibble(output)

  return(output)

}

output <- multiColStringReplace(test_target, test_config)
output

result <- dplyr::tibble(
  col1 = c("Foo", "bar", "", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", ""),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)
output == result

# compare with old method
old <- test_target
for (i in seq(nrow(test_config))) {
  old <- dplyr::mutate_at(old,
                          .vars = dplyr::vars(
                            tidyselect::matches(test_config$string_col[[i]])),
                          .funs = dplyr::funs(
                            stringr::str_replace_all(
                              ., test_config$pattern[[i]], 
                              test_config$replacement[[i]]))
  )
}
old == result

# speed improves, but complexity rises
microbenchmark::microbenchmark("old" = {
  old <- test_target
  for (i in seq(nrow(test_config))) {
    old <- dplyr::mutate_at(old,
                            .vars = dplyr::vars(
                              tidyselect::matches(test_config$string_col[[i]])),
                            .funs = dplyr::funs(
                              stringr::str_replace_all(
                                ., test_config$pattern[[i]], 
                                test_config$replacement[[i]]))
    )
  }
},
"data.table" = {
  multiColStringReplace(test_target, test_config)
}, times = 20)

答案 2 :(得分:0)

为了后代的缘故,如果我将test_target标记作为列表传递给pmap_dfr,我也可以完成此任务(但这不是一个好的解决方案):

purrr::pmap_dfr(
  list(list(test_target),
       test_config$string_col,
       test_config$pattern,
       test_config$replacement),
  testFun
) %>% dplyr::distinct()

尽管有效,但这不是一个好的解决方案,因为它会回收test_target列表中的元素,并在通过参数前进时有效地为test_config的每一行制作一个test_target tibble副本,然后将其绑定产生的4个小标题的所有行一起构成一个较大的最终输出小标题(我正在用distinct()向下过滤。

也许可以采取某种类似<<-之类的方法来避免重复目标小节,但这更加奇怪和糟糕。

答案 3 :(得分:0)

仅供参考,基准测试结果-@camille建议的“笨拙整洁”方法是我的硬件上的胜利者!

Unit: milliseconds
          expr       min        lq      mean    median        uq      max neval
          loop 14.808278 16.098818 17.937283 16.811716 20.438360 24.38021    20
 pmap_function  9.486146 10.157526 10.978879 10.628205 11.112485 15.39436    20
     nice_tidy  8.313868  8.633266  9.597485  8.986735  9.870532 14.32946    20
  awkward_tidy  1.535919  1.639706  1.772211  1.712177  1.783465  2.87615    20
    data.table  5.611538  5.652635  8.323122  5.784507  6.359332 51.63031    20