【问题标题】:bunch recoding of variables in the tidyverse (functional / meta-programing)tidyverse 中变量的一堆重新编码(功能/元编程)
【发布时间】:2019-10-31 08:22:15
【问题描述】:

我想用尽可能少的函数调用来重新编码一堆变量。我有一个 data.frame,我想在其中重新编码许多变量。我创建了一个包含所有变量名称的命名列表以及我想要执行的重新编码参数。在这里我使用mapdpylr 没有问题。然而,当涉及到重新编码时,我发现使用car 包中的recode 比使用dpylr 自己的重新编码功能要容易得多。一个附带的问题是,是否有一种很好的方法可以用dplyr::recode 做同样的事情。

下一步,我将 data.frame 分解为嵌套的 tibble。在这里,我想在每个子集中进行特定的重新编码。这就是事情变得复杂的地方,我无法再在dpylr 管道中执行此操作了。我唯一得到的工作是一个非常丑陋的嵌套for loop

寻找以一种简洁明了的方式完成此任务的想法。

让我们从简单的例子开始:

library(carData)
library(dplyr)
library(purrr)
library(tidyr)

# global recode list
recode_ls = list(

  mar = "'not married' = 0;
          'married' = 1",

  wexp = "'no' = 0;
          'yes' = 1"
)

recode_vars <- names(Rossi)[names(Rossi) %in% names(recode_ls)]

Rossi2 <- Rossi # lets save results under a different name

Rossi2[,recode_vars] <- recode_vars %>% map(~ car::recode(Rossi[[.x]],
                                                          recode_ls[.x],
                                                          as.factor = FALSE,
                                                          as.numeric = TRUE))

到目前为止,这对我来说似乎很干净,除了 car::recode 比 dplyr::recode 更容易使用之外。

我的实际问题来了。我要做的是在每个 tibble 子集中以不同的方式重新编码(在这个简单的示例中)变量 marwexp。在我的真实数据集中,我想在每个子集中重新编码的变量更多,而且名称也不同。有没有人知道如何使用dpylr 管道和map 做到这一点又好又干净?

    nested_rossi <- as_tibble(Rossi) %>% nest(-race)

    recode_wexp_ls = list(

      no = list(

      mar = "'not married' = 0;
             'married' = 1",

      wexp = "'no' = 0;
              'yes' = 1"
      ),

      yes = list(
        mar = "'not married' = 1;
               'married' = 2",

        wexp = "'no' = 1;
                'yes' = 2"
      )

我们也可以将列表附加到嵌套的 data.frame 中,但我不确定这是否会提高效率。

nested_rossi$recode = list(

          no = list(

          mar = "'not married' = 0;
                 'married' = 1",

          wexp = "'no' = 0;
                  'yes' = 1"
          ),

          yes = list(
            mar = "'not married' = 1;
                   'married' = 2",

            wexp = "'no' = 1;
                    'yes' = 2"
          )
        )

【问题讨论】:

  • 也许加入一个查找表,其中的列引用了您想要的子集、起始值和结束值?

标签: r tidyverse purrr recode


【解决方案1】:

感谢您提出一个很酷的问题!这是一个充分利用元编程的好机会。

首先,让我们检查recode() 函数。它获取一个向量和任意数量的(命名的)参数,并返回相同的向量,其值替换为函数参数:

x <- c("a", "b", "c")
recode(x, a = "Z", c = "X")

#> [1] "Z" "b" "X"

recode 的帮助说我们可以使用 unquote splicing (!!!) 将命名列表传递给它。

x_codes <- list(a = "Z", c = "X")
recode(x, !!!x_codes)

#> [1] "Z" "b" "X"

在改变数据框时可以使用此功能。 建议,我们有一个罗西数据集的子集:

library(carData)
library(tidyverse)

rossi <- Rossi %>% 
  as_tibble() %>% 
  select(mar, wexp)

要在单个函数调用中改变两个变量,我们可以使用这个 sn-p(请注意,命名参数和取消引用拼接方法都很好):

mar_codes <- list(`not married` = 0, married = 1)
wexp_codes <- list(no = 0, yes = 1)

rossi %>% 
  mutate(
    mar_code = recode(mar, "not married" = 0, "married" = 1),
    wexp_code = recode(wexp, !!!wexp_codes)
  )

#> # A tibble: 432 x 4
#>    mar         wexp  mar_code wexp_code
#>    <fct>       <fct>    <dbl>     <dbl>
#>  1 not married no           0         0
#>  2 not married no           0         0
#>  3 not married yes          0         1
#>  4 married     yes          1         1
#>  5 not married yes          0         1

因此,取消引用拼接是在非标准评估环境中将多个参数传递给函数的好方法。

现在建议我们有一个代码列表:

mapping <- list(mar = mar_codes, wexp = wexp_codes)
mapping

#> $mar
#> $mar$`not married`
#> [1] 0

#> $mar$married
#> [1] 1

#> $wexp
#> $wexp$no
#> [1] 0

#> $wexp$yes
#> [1] 1

我们需要将此列表转换为表达式列表以放置在 mutate() 中:

expressions <- mapping %>% 
  imap(
    ~ quo(
      recode(!!sym(.y), !!!.x)
    )
  )

expressions

#> $mar
#> <quosure>
#> expr: ^recode(mar, not married = 0, married = 1)
#> env:  0x7fbf374513c0

#> $wexp
#> <quosure>
#> expr: ^recode(wexp, no = 0, yes = 1)
#> env:  0x7fbf37453468

最后一步。在 mutate 中传递这个表达式列表,看看它会做什么:

mutate(rossi, !!!expressions)

#> # A tibble: 432 x 2
#>      mar  wexp
#>    <dbl> <dbl>
#>  1     0     0
#>  2     0     0
#>  3     0     1
#>  4     1     1
#>  5     0     1

现在您可以扩大要重新编码的变量列表、一次处理多个列表等等。

借助如此强大的技术(元编程),您可以做出令人惊奇的事情。 我强烈建议您深入研究这个主题。 没有比 Hadley Wickham's Advanced R book 更好的资源了。

希望,这就是您一直在寻找的。​​p>

更新

潜水更深。问题是:如何将这种技术应用于 tibble-column?

让我们创建 groupdf 的嵌套 tibble(我们要重新编码的数据)

rossi <- 
  head(Rossi, 5) %>% 
  as_tibble() %>% 
  select(mar, wexp)

nested <- tibble(group = c("yes", "no"), df = list(rossi))

nested 看起来像:

# A tibble: 2 x 2
  group df              
  <chr> <list>          
1 yes   <tibble [5 × 2]>
2 no    <tibble [5 × 2]>

我们已经知道如何根据代码列表构建表达式列表。 让我们创建一个函数来为我们处理它。

build_recode_expressions <- function(list_of_codes) {
  imap(list_of_codes, ~ quo(recode(!!sym(.y), !!!.x)))
}

在那里,list_of_codes 参数是需要重新编码的每个变量的命名列表。

假设我们有一个包含多个重新编码codes 的列表,我们可以将其转换为多个表达式列表的列表。每个列表中的变量数量可以是任意的。

codes <- list(
  yes = list(mar = list(`not married` = 0, married = 1)),
  no = list(
    mar = list(`not married` = 10, married = 20), 
    wexp = list(no = "NOOOO", yes = "YEEEES")
  )
)

exprs <- map(codes, build_recode_expressions)

现在我们可以轻松地将exprs 作为新的列表列添加到嵌套数据框中。

还有另一个功能可能对进一步的工作有用。 这个函数接受一个数据框和一个引用的表达式列表 并返回一个带有重新编码列的新数据框。

recode_df <- function(df, exprs) mutate(df, !!!exprs)

是时候将所有内容结合在一起了。 我们有 tibble-column df、list-column exprs 和函数 recode_df 将它们一一绑定在一起。

线索是map2 函数。它允许我们同时迭代两个列表:

nested %>% 
  mutate(exprs = exprs) %>% 
  mutate(df_recoded = map2(df, exprs, recode_df)) %>% 
  unnest(df, df_recoded)

这是输出:

# A tibble: 10 x 5
   group mar         wexp   mar1 wexp1 
   <chr> <fct>       <fct> <dbl> <chr> 
 1 yes   not married no        0 no    
 2 yes   not married no        0 no    
 3 yes   not married yes       0 yes   
 4 yes   married     yes       1 yes   
 5 yes   not married yes       0 yes   
 6 no    not married no       10 NOOOO 
 7 no    not married no       10 NOOOO 
 8 no    not married yes      10 YEEEES
 9 no    married     yes      20 YEEEES
10 no    not married yes      10 YEEEES

希望这次更新能解决你的问题。

【讨论】:

  • 非常感谢您的回复。您基本上回答了我的第一个问题,如何在处理单个 data.frame / tibble 时用 dplyr::recode 替换 car::recode。乍一看,dplyr::recode 的语法似乎不如在 dplyr 管道中使用 car::recode 简单且可读性差。此外,我还没有理解如何将 dplyr::recode 应用于(嵌套)小标题列表,以在每个嵌套小标题中重新编码不同的变量(和/或相同的变量)。稍后,我将更新我的问题以使其更加明确。
  • 感谢您更新的答案。我没有想到在整个嵌套的小标题上使用mutate,然后在map2 中使用自定义函数重新编码。很好的解决方案!在更改单个数据帧时,我会三思而后行是否使用 dplyr::recode,但在一步重新编码多个数据集时绝对值得付出努力。最后一个问题:为什么不能用mutate(df_recoded = map2(df, exprs, ~ mutate(.x, !!! .y))) %&gt;%替换mutate(df_recoded = map2(df, exprs, recode_df)) %&gt;% ?它抛出一个错误错误in quos(...) : object '.y' not found
  • 是的,我遇到了同样的问题。我认为这是由于 recode_df() 函数中引用期间环境处理不当造成的。这可能是更深入调查的主题。
猜你喜欢
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 2020-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多