在 dplyr 链中创建指标变量列

Creating indicator variable columns in dplyr chain

已更新:向那些回答我的人道歉,在我原来的例子中我忽略了一个事实 data.frame() 创建了 var 作为一个因素而不是作为一个字符向量,正如我所期望的那样。我已经更正了示例,这至少会破坏其中一个答案。

--原创--

我有一个数据框,我正在对其执行一系列 dplyrtidyr 操作,我想添加将被编码为 0 或 1 的指示变量的列,并在 dplyr 链 中执行此操作 。因子的每个级别(目前存储为字符向量)应在单独的列中编码,列名是固定前缀与变量级别的串联,例如var 的级别为 a,新列 var_a 将为 1,而 var_a 的所有其他行将为 0。

下面这个使用 base R 的最小示例产生了我想要的结果(感谢 this blog post),但我想将它全部放入 dplyr 链,并不太清楚该怎么做。

library(dplyr)
df <- data.frame(var = sample(x = letters[1:4], size = 10, replace = TRUE), stringsAsFactors = FALSE)
for(level in unique(df$var)){
  df[paste("var", level, sep = "_")] <- ifelse(df$var == level, 1, 0)
}

请注意,真实数据集包含多列,其中 none 应在创建指标变量时更改或删除,但列 var 除外,它可以转换为输入 因子.

不太漂亮,但这个功能应该可以用

dummy <- function(data, col) {
    for(c in col) {
        idx <- which(names(data)==c)
        v <- data[[idx]]
        stopifnot(class(v)=="factor")
        m <- matrix(0, nrow=nrow(data), ncol=nlevels(v))
        m[cbind(seq_along(v), as.integer(v))]<-1
        colnames(m) <- paste(c, levels(v), sep="_")
        r <- data.frame(m)
        if ( idx>1 ) {
            r <- cbind(data[1:(idx-1)],r)
        }
        if ( idx<ncol(data) ) {
            r <- cbind(r, data[(idx+1):ncol(data)])
        }
        data <- r
    }
    data
}

这是一个示例 data.frame

dd <- data.frame(a=runif(30),
    b=sample(letters[1:3],30,replace=T),
    c=rnorm(30),
    d=sample(letters[10:13],30,replace=T)
)

然后您将要扩展的列指定为字符向量。你可以做到

dd %>% dummy("b")

dd %>% dummy(c("b","d"))

一个函数成为 dplyr 管道的一部分的唯一要求是它需要一个数据帧作为输入,returns 一个数据帧作为输出。因此,利用 model.matrix:

make_inds <- function(df, cols=names(df))
{
    # do each variable separately to get around model.matrix dropping aliased columns
    do.call(cbind, c(df, lapply(cols, function(n) {
        x <- df[[n]]
        mm <- model.matrix(~ x - 1)
        colnames(mm) <- gsub("^x", paste(n, "_", sep=""), colnames(mm))
        mm
    })))
}

# insert into pipeline
data %>% ... %>% make_inds %>% ...

尽管确实需要 lapply,但无需创建函数也是可能的。如果 var 是一个因素,您可以使用它的级别;我们可以将它的列绑定到 lapply,它在 var 的级别上循环并创建值,用 setNames 命名它们,并将它们转换为 tbl_df.

df %>% bind_cols(as_data_frame(setNames(lapply(levels(df$var), 
                                               function(x){as.integer(df$var == x)}), 
                                        paste0('var2_', levels(df$var)))))

returns

Source: local data frame [10 x 5]

      var var_d var_c var2_c var2_d
   (fctr) (dbl) (dbl)  (int)  (int)
1       d     1     0      0      1
2       c     0     1      1      0
3       c     0     1      1      0
4       c     0     1      1      0
5       d     1     0      0      1
6       d     1     0      0      1
7       c     0     1      1      0
8       c     0     1      1      0
9       d     1     0      0      1
10      c     0     1      1      0

如果 var 是一个字符向量,而不是一个因素,你可以做同样的事情,但使用 unique 而不是 levels:

df %>% bind_cols(as_data_frame(setNames(lapply(unique(df$var), 
                                               function(x){as.integer(df$var == x)}), 
                                        paste0('var2_', unique(df$var)))))

两个注意事项:

  • 无论数据类型如何,此方法都适用,但速度较慢。在您的数据足够大的情况下,无论如何将数据存储为 factor 可能是有意义的,因为它包含很多重复的级别。
  • 两个版本都从 df$var 中提取数据,因为它存在于调用环境中,而不是因为它可能存在于更大的链中,并且假设 var 在传递的任何内容中都没有变化。就我所见,除了 dplyr 的正常 NSE 之外,引用 var 的动态值是相当痛苦的。

另一种更简单且 factor 不可知的替代方法,使用 reshape2::dcast:

library(reshape2)
df %>% cbind(1 * !is.na(dcast(df, seq_along(var) ~ var, value.var = 'var')[,-1]))

它仍然从调用环境中提取 df 的版本,所以链实际上只决定了你加入的是什么。因为它使用 cbind 而不是 bind_cols,结果也将是 data.frame,而不是 tbl_df,所以如果你想保留所有 tbl_df(如果数据很大,则很聪明),您需要将 cbind 替换为 bind_cols(as_data_frame( ... ))bind_cols 似乎不想为您进行转换。

但是请注意,虽然此版本更简单,但速度相对较慢,两者都在 factor 数据上:

Unit: microseconds
   expr      min        lq      mean    median       uq      max neval
 factor  358.889  384.0010  479.5746  427.9685  501.580 3995.951   100
 unique  547.249  585.4205  696.4709  633.4215  696.402 4528.099   100
  dcast 2265.517 2490.5955 2721.1118 2628.0730 2824.949 3928.796   100

和字符串数据:

Unit: microseconds
   expr      min       lq      mean    median        uq      max neval
 unique  307.190  336.422  414.1031  362.6485  419.3625 3693.340   100
  dcast 2117.807 2249.077 2517.0417 2402.4285 2615.7290 3793.178   100

对于小数据,这无关紧要,但对于更大的数据,可能值得忍受复杂性。

我首先进行此问答是因为我真的想将 model.matrix 放入 magrittr 管道工作流程中,或者仅使用 tidyverse 函数(抱歉,baseRs)生成等效输出。

后来,我找到了 ,它优雅地使用了我 认为 可能实现的功能(但我不是自己想出来的) ):

df <- data_frame(var = sample(x = letters[1:4], size = 10, replace = TRUE))

df %>% 
  mutate(unique_row_id = 1:n()) %>% #The rows need to be unique for `spread` to work.
  mutate(dummy = 1) %>% 
  spread(var, dummy, fill = 0)

所以,我添加了一个 updated/modified 版本的链接解决方案,这样首先到达这里的人就不必继续寻找(就像我一样)。