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

时间:2019-06-17 17:46:48

标签: r tidyverse purrr recode

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

下一步,我将data.frame分解为嵌套的小标题。在这里,我想对每个子集进行特定的重新编码。这是事情变得复杂的地方,我再也无法在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更易于使用之外,这对我来说似乎还很干净。

这是我的实际问题。我试图做的是(在这个简单的示例中)重新编码每个小对象子集中的变量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"
          )
        )

1 个答案:

答案 0 :(得分:1)

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

首先,让我们研究一下recode()函数。它获得一个向量和任意数量的(命名)参数,并返回相同的向量,并用函数参数替换其值:

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

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

recode的帮助说,我们可以使用unquote拼接(!!!)将命名列表传递到其中。

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)

要在单个函数调用中对两个变量进行变异,我们可以使用此代码段(请注意,命名参数和unquote拼接方法都可以正常工作):

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更好的启动资源了。

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

更新

深入研究。问题是:如何将这种技术应用于小标题列?

让我们创建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作为新的列表列添加到嵌套数据框中。

还有另一个功能可能对以后的工作很有用。 该函数需要一个数据框和一个带引号的表达式列表 并返回带有重新编码的列的新数据框。

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

是时候将所有内容组合在一起了。 我们有小标题列df,列表列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

我希望此更新能够解决您的问题。