这个问题与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来完成我想做的事情吗,还是有其他解决方法或更好的方法来解决问题?
谢谢!
答案 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)
我对purrr
和dplyr
并没有经验,但是这里是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