自动将R因子扩展为每个因子水平的1/0指示变量集合

117
我有一个包含因子的 R 数据框,我希望将其“展开”,以便于在新数据框中为每个因子水平创建一个对应列,其中包含 1/0 指示器。例如,假设我有以下数据框:
df.original <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c(1,2,3,4))

我想要:

df.desired  <- data.frame(foo = c(1,1,0,0), bar=c(0,0,1,1), ham=c(1,2,3,4))

因为对于某些分析,需要完全由数字组成的数据框(例如主成分分析),所以我认为这个功能可能已经被内置。编写一个函数来做这件事不应该太难,但我可以预见到一些与列名相关的挑战。如果已经存在类似的东西,我宁愿使用那个。

10个回答

138
使用model.matrix函数:
model.matrix( ~ Species - 1, data=iris )

1
我能否补充一下,对于我来说,这种方法比使用“cast”快得多。 - Matt Weller
4
我已经审查了?formula?model.matrix的第二段,但不太明确(可能是我在矩阵代数和模型制定方面知识不够深)。经过更多调查,我发现-1只是指定不包括“截距”列。如果你省略-1,你会在输出中看到一个由1组成的截距列,还有一列二进制列被遗漏了。你可以根据其他列的值为0的行来确定被遗漏的列的值为1的位置。文档似乎比较晦涩 - 是否有其他好的资源可用? - Ryan Chase
1
@RyanChase,有很多关于R/S的在线教程和书籍(其中一些在r-project.org网页上有简要描述)。我自己对S和R的学习是相当杂乱无章的(而且时间漫长),所以我不是最适合就目前的书籍/教程如何吸引初学者发表意见的人。然而,我是一个实验的粉丝。在全新的R会话中尝试一些东西可以非常启迪,并且不危险(我遇到的最糟糕的情况就是崩溃了R,但那也很少,导致R得到了改进)。Stackoverflow是理解发生了什么的好资源。 - Greg Snow
8
如果您想转换所有的因子列,您可以使用以下代码:model.matrix(~., data=iris)[,-1] - user890739
1
@colin,虽然不是完全自动化的,但你可以在使用na.exclude后使用naresid将缺失值放回去。一个快速的例子:tmp <- data.frame(x=factor(c('a','b','c',NA,'a'))); tmp2 <- na.exclude(tmp); tmp3 <- model.matrix( ~x-1, tmp2); tmp4 <- naresid(attr(tmp2,'na.action'), tmp3) - Greg Snow
显示剩余6条评论

18
如果您的数据框只包含因子变量(或者您正在处理的变量子集都是因子),则还可以使用ade4软件包中的acm.disjonctif函数:
R> library(ade4)
R> df <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c("red","blue","green","red"))
R> acm.disjonctif(df)
  eggs.bar eggs.foo ham.blue ham.green ham.red
1        0        1        0         0       1
2        0        1        1         0       0
3        1        0        0         1       0
4        1        0        0         0       1

虽然不完全符合您所描述的情况,但它也可能很有用...


谢谢,这对我很有帮助,因为它使用的内存比model.matrix少! - Serhiy
我喜欢变量的命名方式;但我不喜欢它们以占用存储空间的数字形式返回,而应该(在我看来)只是逻辑值。 - dsz

8

使用 reshape2 包的快速方法:

require(reshape2)

> dcast(df.original, ham ~ eggs, length)

Using ham as value column: use value_var to override.
  ham bar foo
1   1   0   1
2   2   0   1
3   3   1   0
4   4   1   0

请注意,这将精确地生成您想要的列名。

不错。但要注意ham的重复。比如,d <- data.frame(eggs = c("foo", "bar", "foo"), ham = c(1,2,1)); dcast(d, ham ~ eggs, length) 会使foo = 2。 - kohske
1
@Kohske,没错,但我假设ham是唯一的行ID。如果ham不是唯一的ID,则必须使用其他唯一ID(或创建虚拟ID)并将其用于ham的位置。将分类标签转换为二进制指示符仅对唯一ID有意义。 - Prasad Chalasani

7

可能虚拟变量与您想要的类似。然后,model.matrix很有用:

> with(df.original, data.frame(model.matrix(~eggs+0), ham))
  eggsbar eggsfoo ham
1       0       1   1
2       0       1   2
3       1       0   3
4       1       0   4

6
来自nnet包的一个晚期条目class.ind
library(nnet)
 with(df.original, data.frame(class.ind(eggs), ham))
  bar foo ham
1   0   1   1
2   0   1   2
3   1   0   3
4   1   0   4

4

我刚刚看到这个旧帖子,想要添加一个使用ade4的函数,它可以将由因子和/或数字数据组成的数据框转换为带有因子虚拟编码的数据框。

dummy <- function(df) {  

    NUM <- function(dataframe)dataframe[,sapply(dataframe,is.numeric)]
    FAC <- function(dataframe)dataframe[,sapply(dataframe,is.factor)]

    require(ade4)
    if (is.null(ncol(NUM(df)))) {
        DF <- data.frame(NUM(df), acm.disjonctif(FAC(df)))
        names(DF)[1] <- colnames(df)[which(sapply(df, is.numeric))]
    } else {
        DF <- data.frame(NUM(df), acm.disjonctif(FAC(df)))
    }
    return(DF)
} 

让我们试试吧。

df <-data.frame(eggs = c("foo", "foo", "bar", "bar"), 
            ham = c("red","blue","green","red"), x=rnorm(4))     
dummy(df)

df2 <-data.frame(eggs = c("foo", "foo", "bar", "bar"), 
            ham = c("red","blue","green","red"))  
dummy(df2)

3

以下是一种更加清晰的方法。我使用model.matrix来创建虚拟布尔变量,然后将其合并回原始数据框中。

df.original <-data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c(1,2,3,4))
df.original
#   eggs ham
# 1  foo   1
# 2  foo   2
# 3  bar   3
# 4  bar   4

# Create the dummy boolean variables using the model.matrix() function.
> mm <- model.matrix(~eggs-1, df.original)
> mm
#   eggsbar eggsfoo
# 1       0       1
# 2       0       1
# 3       1       0
# 4       1       0
# attr(,"assign")
# [1] 1 1
# attr(,"contrasts")
# attr(,"contrasts")$eggs
# [1] "contr.treatment"

# Remove the "eggs" prefix from the column names as the OP desired.
colnames(mm) <- gsub("eggs","",colnames(mm))
mm
#   bar foo
# 1   0   1
# 2   0   1
# 3   1   0
# 4   1   0
# attr(,"assign")
# [1] 1 1
# attr(,"contrasts")
# attr(,"contrasts")$eggs
# [1] "contr.treatment"

# Combine the matrix back with the original dataframe.
result <- cbind(df.original, mm)
result
#   eggs ham bar foo
# 1  foo   1   0   1
# 2  foo   2   0   1
# 3  bar   3   1   0
# 4  bar   4   1   0

# At this point, you can select out the columns that you want.

0
我需要一个更加灵活的“分裂”因子的函数,并基于ade4包中的acm.disjonctif函数创建了一个函数。 这使您可以选择要分裂的值,在acm.disjonctif中为0和1。它仅会分裂具有“少量”水平的因子。数字列将被保留。
# Function to explode factors that are considered to be categorical,
# i.e., they do not have too many levels.
# - data: The data.frame in which categorical variables will be exploded.
# - values: The exploded values for the value being unequal and equal to a level.
# - max_factor_level_fraction: Maximum number of levels as a fraction of column length. Set to 1 to explode all factors.
# Inspired by the acm.disjonctif function in the ade4 package.
explode_factors <- function(data, values = c(-0.8, 0.8), max_factor_level_fraction = 0.2) {
  exploders <- colnames(data)[sapply(data, function(col){
      is.factor(col) && nlevels(col) <= max_factor_level_fraction * length(col)
    })]
  if (length(exploders) > 0) {
    exploded <- lapply(exploders, function(exp){
        col <- data[, exp]
        n <- length(col)
        dummies <- matrix(values[1], n, length(levels(col)))
        dummies[(1:n) + n * (unclass(col) - 1)] <- values[2]
        colnames(dummies) <- paste(exp, levels(col), sep = '_')
        dummies
      })
    # Only keep numeric data.
    data <- data[sapply(data, is.numeric)]
    # Add exploded values.
    data <- cbind(data, exploded)
  }
  return(data)
}

0
在编程中,可以使用sapply ==来生成虚拟向量。
x <- with(df.original, data.frame(+sapply(unique(eggs), `==`, eggs), ham))
x
#  foo bar ham
#1   1   0   1
#2   1   0   2
#3   0   1   3
#4   0   1   4

all.equal(x, df.desired)
#[1] TRUE

一种可能更快的变体 - 结果最好用作listdata.frame

. <- unique(df.original$eggs)
with(df.original, 
     data.frame(+do.call(cbind, lapply(setNames(., .), `==`, eggs)), ham))

在一个矩阵中进行索引 - 结果最好用作矩阵

. <- unique(df.original$eggs)
i <- match(df.original$eggs, .)
nc <- length(.)
nr <- length(i)
cbind(matrix(`[<-`(integer(nc * nr), 1:nr + nr * (i - 1), 1), nr, nc,
                 dimnames=list(NULL, .)), df.original["ham"])

使用outer - 结果最好用作矩阵

. <- unique(df.original$eggs)
cbind(+outer(df.original$eggs, setNames(., .), `==`), df.original["ham"])

使用rep - 结果最好用于矩阵

. <- unique(df.original$eggs)
n <- nrow(df.original)
cbind(+matrix(df.original$eggs == rep(., each=n), n, dimnames=list(NULL, .)),
 df.original["ham"])

0

虽然这个问题已经有10年了,但为了完整起见...

fixest包中的i()函数正好可以做到这一点。

除了从类似因子的变量创建设计矩阵之外,您还可以很容易地在运行时执行两个额外的操作:

  • 使用参数“bin”对值进行分组,
  • 使用参数ref排除某些因子值。

由于它是为此任务而制作的,如果您的变量恰好是数字,则无需使用factor(x_num)将其包装(与model.matrix解决方案相反)。

以下是一个示例:

library(fixest)
data(airquality)
table(airquality$Month)
#>  5  6  7  8  9 
#> 31 30 31 31 30

head(i(airquality$Month))
#>      5 6 7 8 9
#> [1,] 1 0 0 0 0
#> [2,] 1 0 0 0 0
#> [3,] 1 0 0 0 0
#> [4,] 1 0 0 0 0
#> [5,] 1 0 0 0 0
#> [6,] 1 0 0 0 0

#
# Binning (check out the help, there are many many ways to bin)
#

colSums(i(airquality$Month, bin = 5:6)))
#>  5  7  8  9 
#> 61 31 31 30 

#
# References
#

head(i(airquality$Month, ref = c(6, 9)), 3)
#>      5 7 8
#> [1,] 1 0 0
#> [2,] 1 0 0
#> [3,] 1 0 0

这里有一个小包装器,可以默认扩展所有非数字变量:

library(fixest)

# data: data.frame
# var: vector of variable names // if missing, all non numeric variables
# no argument checking
expand_factor = function(data, var){
    
    if(missing(var)){
        var = names(data)[!sapply(data, is.numeric)]
        if(length(var) == 0) return(data)
    }
    
    data_list = unclass(data)
    new = lapply(var, \(x) i(data_list[[x]]))
    data_list[names(data_list) %in% var] = new
    
    do.call("cbind", data_list)
}

my_data = data.frame(eggs = c("foo", "foo", "bar", "bar"), ham = c(1,2,3,4))

expand_factor(my_data)
#>      bar foo ham
#> [1,]   0   1   1
#> [2,]   0   1   2
#> [3,]   1   0   3
#> [4,]   1   0   4

最后,对于那些好奇的人,这个时间与model.matrix的解决方案是等价的。

library(microbenchmark)
my_data = data.frame(x = as.factor(sample(100, 1e6, TRUE)))

microbenchmark(mm = model.matrix(~x, my_data),
               i = i(my_data$x), times = 5)
#> Unit: milliseconds
#>  expr      min       lq     mean   median       uq      max neval
#>    mm 155.1904 156.7751 209.2629 182.4964 197.9084 353.9443     5
#>     i 154.1697 154.7893 159.5202 155.4166 163.9706 169.2550     5


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接