根据一个列表的分类变量创建虚拟变量

10

我有一个数据框,其中包含一个分类变量,它保存了不定长的字符串列表(这一点很重要,否则这个问题就会成为这个问题这个问题的重复),例如:

df <- data.frame(x = 1:5)
df$y <- list("A", c("A", "B"), "C", c("B", "D", "C"), "E")
df
  x       y
1 1       A
2 2    A, B
3 3       C
4 4 B, D, C
5 5       E

所需的形式是对于在 df$y 中出现的任何唯一字符串,创建一个虚拟变量,即:

data.frame(x = 1:5, A = c(1,1,0,0,0), B = c(0,1,0,1,0), C = c(0,0,1,1,0), D = c(0,0,0,1,0), E = c(0,0,0,0,1))
  x A B C D E
1 1 1 0 0 0 0
2 2 1 1 0 0 0
3 3 0 0 1 0 0
4 4 0 1 1 1 0
5 5 0 0 0 0 1

这种天真的方法可行:

> uniqueStrings <- unique(unlist(df$y))
> n <- ncol(df)
> for (i in 1:length(uniqueStrings)) {
+   df[,  n + i] <- sapply(df$y, function(x) ifelse(uniqueStrings[i] %in% x, 1, 0))
+   colnames(df)[n + i] <- uniqueStrings[i]
+ }

然而,对于大数据框来说,它非常丑陋、懒惰和缓慢。

有什么建议吗?tidyverse里的一些花哨东西?


更新:下面我列出了3种不同的方法。我在我的笔记本电脑上(Windows 7,32GB RAM)使用真实数据集测试了它们,该数据集包含100万行,每行包含一个长度为1到4的字符串列表(共约350个唯一字符串值),总大小为200MB。因此预期结果是一个1M x 350的数据框。tidyverse (@Sotos) 和 base (@joel.wilson) 方法运行时间太长,导致我不得不重新启动R。然而,qdapTools (@akrun) 的方法表现非常出色:

> system.time(res1 <- mtabulate(varsLists))
   user  system elapsed 
  47.05   10.27  116.82

所以这就是我将采纳的方法。

返回已翻译的文本:data.frame(x = df$x, t(sapply(df$y, function(l){table(factor(l, levels = LETTERS[1:5]))}))) - alistaire
@alistaire 或许应该使用 levels = unique(unlist(df$y)) 而不是 LETTERS[1:5] - Sotos
@Sotos 我之前也有这个想法,但是我认为这样会减少计算量。最好的方法是将其存储为单独的变量,但这需要另一行代码... - alistaire
@alistaire 真的 - Sotos
3个回答

7

另一个想法是,

library(dplyr)
library(tidyr)

df %>% 
 unnest(y) %>% 
 mutate(new = 1) %>% 
 spread(y, new, fill = 0) 

#  x A B C D E
#1 1 1 0 0 0 0
#2 2 1 1 0 0 0
#3 3 0 0 1 0 0
#4 4 0 1 1 1 0
#5 5 0 0 0 0 1

除了您在评论中提到的案例,我们还可以使用reshape2中的dcast,因为它比spread更加灵活。

df2 <- df %>% 
        unnest(y) %>% 
        group_by(x) %>% 
        filter(!duplicated(y)) %>% 
        ungroup()

reshape2::dcast(df2, x ~ y, value.var = 'y', length)

#  x A B C D E
#1 1 1 0 0 0 0
#2 2 1 1 0 0 0
#3 3 0 0 1 0 0
#4 4 0 1 1 1 0
#5 5 0 0 0 0 1

#or with df$x <- c(1, 1, 2, 2, 3)

#  x A B C D E
#1 1 1 1 0 0 0
#2 2 0 1 1 1 0
#3 3 0 0 0 0 1

#or with df$x <- rep(1,5)

#  x A B C D E
#1 1 1 1 1 1 1

感谢,当 df$x = rep(1, 5) 时请查看发生了什么。"错误:行(1、2)、(3、5)、(4、7)存在重复标识符"。 - Giora Simchoni
在这种情况下,您的预期结果会是什么?类似于 df %>% unnest(y) %>% group_by(x) %>% mutate(new = 1:n()) %>% spread(y, x, fill = 0) 这样的内容? - Sotos
它适用于 df$x = rep(1, 5) 的情况。在原始的 df$x = 1:5 情况下,它会给出“错误:行(1、2)的标识符重复”。 - Giora Simchoni
1
在使用 group_by() 之前尝试使用 mutate(new = 1:n()) - Sotos
@GioraSimchoni,你现在可以检查一下吗? - Sotos
显示剩余2条评论

6
我们可以使用mtabulate
library(qdapTools)
cbind(df[1], mtabulate(df$y))
#  x A B C D E
#1 1 1 0 0 0 0
#2 2 1 1 0 0 0
#3 3 0 0 1 0 0
#4 4 0 1 1 1 0
#5 5 0 0 0 0 1

这真是令人印象深刻,而且速度超快(在我的电脑上处理大约1M行数据和350个唯一值只需要几秒钟)。你有没有不需要整个新包的解决方案?谢谢。 - Giora Simchoni
@GioraSimchoni 看起来好像有人在没有使用包的情况下回答了它。 - akrun
2
@GioraSimchoni,我猜一个基本的替代方案是 table(rep(df$x, lengths(df$y)), unlist(df$y)) - alexis_laz
不适用于 df$x = rep(1,5)df$x = c(1,1,2,2,3)df$x是什么并不重要。 - Giora Simchoni
1
抱歉@akrun,我是指alexis_laz的评论。 - Giora Simchoni
显示剩余2条评论

2
这不涉及任何外部包。
# thanks to Sotos for suggesting to use `unique(unlist(df$y))` instead of `LETTERS[1!:5]`
sapply(unique(unlist(df$y)), function(j) as.numeric(grepl(j, df$y)))
#     A B C D E
#[1,] 1 0 0 0 0
#[2,] 1 1 0 0 0
#[3,] 0 0 1 0 0
#[4,] 0 1 1 1 0
#[5,] 0 0 0 0 1

2
LETTERS 部分有问题。你可以使用 unique(unlist(df$y)) 代替。 - Sotos
无法与 df$x = rep(1,5)df$x = c(1,1,2,2,3) 一起使用。df$x 是什么并不重要。 - Giora Simchoni
1
@joel.wilson非常好用,我会进行一些基准测试,看看它与其他“高级”解决方案相比如何,谢谢。 - Giora Simchoni
@GioraSimchoni 它的性能如何? - joel.wilson

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