tidyverse 中变量的一堆重新编码(功能/元编程)

bunch recoding of variables in the tidyverse (functional / meta-programing)

我想用尽可能少的函数调用重新编码一堆变量。我有一个 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"
          )
        )

感谢您提出一个很酷的问题!这是使用元编程的所有力量的绝好机会。

首先,让我们检查一下recode()函数。它得到一个向量和任意数量的(命名的)参数和 returns 相同的向量,其值替换为函数参数:

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

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

recode的帮助说我们可以使用反引号拼接(!!!)将命名列表传递给它。

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

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

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

library(carData)
library(tidyverse)

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

要在单个函数调用中改变两个变量,我们可以使用此代码段(请注意,命名参数和不引用拼接方法都很有效):

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

因此,反引号拼接是在 non-standard 评估环境中将多个参数传递给函数的好方法。

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

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.

更好的开始资源了

希望,这就是您一直在寻找的东西。

更新

深入研究。问题是:如何将此技术应用于 tibble-column?

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

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 作为新的 list-column 添加到嵌套数据框中。

还有一个函数可能对进一步的工作有用。 此函数采用数据框和引用表达式列表 和 returns 一个带有重新编码列的新数据框。

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

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