首页 > 解决方案 > 如何避免for-loop *or*如何在迭代过程中动态更新purrr::map得到的结果?

问题描述

来自 Stata,我有时仍在为 R 的不同编程方法而苦苦挣扎。尤其是在避免for循环方面。

在下面的示例中,我编写了两个函数来覆盖ex$status1' and ex$status2' 的原始值。对于每个 id,如果在各自的 id 中x出现 ,则应将两个变量的原始值替换为 。x

该函数myfunc2完全能够为多个变量执行此任务(在下面的示例中:status1status2)。

但是,我的问题发生在尝试强制替换初始值的顺序时。顺序为c(1,5,3,7)。也就是说,如果对于给定的 id 观察到值 1,则该 id 的所有变量值都应替换为 1。然后应针对 的剩余值对更新的数据重复该过程c(1,5,3,7)。我使用 for 循环完成了这项任务,但未能使用其中一个purrrmap 函数来完成,因为这些函数总是在原始 tibble 上执行并且没有按顺序更新 tibble(参见下面的代码)。谁能告诉我,如何使用 map 函数(或不使用for循环)获得所需的结果?

ex <- tibble(id = c(1,1,1,1,2,2,2),
             status1 = c(3,3,5,7,1,5,7),
             status2 = c(3,3,3,7,7,5,7))

ex

myfunc <- function(df, id, var, val) {
  df <- df %>% 
    group_by(id) %>%
    mutate({{var}} := case_when(any({{var}} == val) ~ val,
                                TRUE ~ {{var}})) %>%
    ungroup() %>% 
    select({{var}})
  return(df)
}

myfunc(ex, id, status1, 1) 



myfunc2 <- function(df, id, var, val) {
  map_dfc(var,
          ~myfunc(df, id, !!sym(.x), val)) %>%
    add_column(id = df$id, .before = 1)
}



myfunc2(ex, id, c("status1", "status2"), 1)


# this works
for (i in c(1,5,3,7)) {
  ex <- myfunc2(ex, id, c("status1", "status2"), i)
}

# this does not work
c(1,5,3,7) %>%
  map_dfc(function(x) {ex <- myfunc2(ex, id, c("status1", "status2"), x)})

# original data
# A tibble: 7 x 3
     id status1 status2
  <dbl>   <dbl>   <dbl>
1     1       3       3
2     1       3       3
3     1       5       3
4     1       7       7
5     2       1       7
6     2       5       5
7     2       7       7 

# Data after executing the for-loop
# A tibble: 7 x 3
     id status1 status2
  <dbl>   <dbl>   <dbl>
1     1       5       3
2     1       5       3
3     1       5       3
4     1       5       3
5     2       1       5
6     2       1       5
7     2       1       5

标签: rfor-looppurrr

解决方案


lapply,map在每个输入元素上循环并返回输出,但它不会像在for循环中那样递归地更新原始对象。如果我们想这样做,则必须进行范围更新,<<-这可能不是最佳选择。会推荐for循环

library(dplyr)
library(purrr)
c(1,5,3,7) %>%
 map_dfc(function(x) {
          ex <<- myfunc2(ex, id, c("status1", "status2"), x)
   })

现在,我们检查对象'ex'

ex
# A tibble: 7 x 3
#     id status1 status2
#  <dbl>   <dbl>   <dbl>
#1     1       5       3
#2     1       5       3
#3     1       5       3
#4     1       5       3
#5     2       1       5
#6     2       1       5
#7     2       1       5
 

使用tidyverse,我们可以使用reduce来代替mapand<<-

reduce(list(1, 5, 3, 7),
      ~myfunc2(.x, id, c("status1", "status2"), .y), .init = ex)
# A tibble: 7 x 3
#     id status1 status2
#  <dbl>   <dbl>   <dbl>
#1     1       5       3
#2     1       5       3
#3     1       5       3
#4     1       5       3
#5     2       1       5
#6     2       1       5
#7     2       1       5

这类似于base R Reduce

Reduce(function(x, y) myfunc2(x, id, c("status1", "status2"), y),
          list(1, 5, 3, 7), init = ex)
# A tibble: 7 x 3
#     id status1 status2
#  <dbl>   <dbl>   <dbl>
#1     1       5       3
#2     1       5       3
#3     1       5       3
#4     1       5       3
#5     2       1       5
#6     2       1       5
#7     2       1       5

这些方法的一个优点是避免了副作用,即我们不必更新原始对象

ex
# A tibble: 7 x 3
#     id status1 status2
#  <dbl>   <dbl>   <dbl>
#1     1       3       3
#2     1       3       3
#3     1       5       3
#4     1       7       7
#5     2       1       7
#6     2       5       5

但是,考虑到for循环的简单性(在理解和执行方面),使用循环可能会更好for(主观意见)


推荐阅读