在dplyr中使用strsplit和subset以及mutate

30

我有一个只有一个字符串列的数据表。我想使用strsplit创建另一列,该列是此列的子集。

dat <- data.table(labels=c('a_1','b_2','c_3','d_4'))

我想要的输出是

label  sub_label
a_1    a
b_2    b
c_3    c
d_4    d 

我已经尝试了下面的方法,但似乎都没有起作用。

dat %>%
    mutate(
        sub_labels=strsplit(as.character(labels), "_")[[1]][1]
    ) 
# gives a column whose values are all "a"

这个对我来说似乎是合理的。

dat %>%
    mutate(
        sub_labels=sapply(strsplit(as.character(labels), "_"), function(x) x[[1]][1])
    )

出现错误

错误:不知道如何处理类型pairlist

我看到另一篇帖子,在该帖子中,从strsplit的输出中复制并粘贴合并可以正常工作,因此我不明白为什么在匿名函数中进行子集设置会出现问题。感谢任何有关此事的阐述。


5
使用正则表达式或substr更简单,因为它们返回字符串而不是列表:dat %>% mutate(sub_label = sub('_.*', '', labels)) 另一个选择是使用tidyr::separateextra='drop'以及remove=FALSEdat %>% separate(labels, 'sub_label', extra='drop', remove=FALSE) - alistaire
3
奇怪,我刚运行了你的最后一段代码“dat %>% mutate(sub_labels=sapply(strsplit(as.character(labels), "_"), function(x) x[[1]][1]))”,它正常工作,没有出现错误。 - Djork
如果你有一个 data.table,只需执行 dat[, c("first","second") := tstrsplit(labels,"_")] - thelatemail
感谢@thelatemail。令人费解的是,即使将输出分配给对象(例如,我将其分配给x,并且必须打印两次x才能看到表格),第一次运行时输出也不会被打印出来,但它非常好用且简洁。 - chungkim271
4个回答

46

tidyr::separate可以帮助处理这里的问题:

> dat %>% separate(labels, c("first", "second") )
   first second
1:     a      1
2:     b      2
3:     c      3
4:     d      4    

虽然这样做不会保留原始列,但我认为问题就在这里。 - thelatemail
4
我认为你可以指定 remove = FALSE 来处理这个问题。 - David Arenburg
太棒了!我不知道这个函数的存在。我正在尝试解决与 OP 相似的问题,这正是我所需要的。谢谢! - Andrew Brēza

15

另一种方法使用purrrmap_chr,我发现它在我不想麻烦地分离和联合(例如将结果与其他字符串一起使用sprintf)时非常有用:

tibble(labels=c('a_1','b_2','c_3','d_4')) %>% 
  mutate(sub_label = stringr::str_split(labels, "_") %>% map_chr(., 1))

根据我的经验,这种方法比 separate 要快得多,特别是当您有更长的数据集时。 当我使用100个字符串时, separate 几乎可以击败map,但在我使用1000个字符串的大多数情况下落后了(不确定那个最大值是什么)。

    > microbenchmark::microbenchmark(
+   d.filtered_reads %>% head(1000) %>% 
+     mutate(name = stringr::str_split(Header, " ") %>% map_chr(., 1)) %>% 
+     select(-Header),
+   d.filtered_reads %>% head(1000) %>% 
+     separate(Header, into = c("name","index"), sep = " ") %>% 
+     select(-"index")
+ )
Unit: milliseconds
                                                                                                                          expr
 d.filtered_reads %>% head(1000) %>% mutate(name = stringr::str_split(Header,      " ") %>% map_chr(., 1)) %>% select(-Header)
          d.filtered_reads %>% head(1000) %>% separate(Header, into = c("name",      "index"), sep = " ") %>% select(-"index")
      min       lq     mean   median       uq       max neval
 5.333891 5.817589 6.292954 5.935706 6.059031 41.530089   100
 7.517316 8.031325 8.399471 8.500359 8.647468  9.855612   100

2
值得一提的是,strsplit 已被 stringr 中的 str_split 取代(https://github.com/tidyverse/stringr)。此外,以下代码也可以作为替代方案:`dat %>% mutate(sub_label = sapply(str_split(labels, "_"), function(x) x[1]))`。 - Sebastian Müller
1
谢谢!我已经添加了stringr::以澄清。通常我只显示我正在加载tidyverse,但我在这里忘记了这样做,所以这是一个重要的澄清。 - GenesRus

10
我并没有想出这个方法,只是在寻找解决方案时偶然发现了这个github问题,认为它比很多答案更简单,特别是避免了额外的map_chr()tmp_chunks
# I used data.frame since I don't have data table installed
library(dplyr)
library(stringr)
dat <- data.frame(labels=c('a_1','b_2','c_3','d_4'))
dat %>% mutate(sub_label = str_split(labels, "_", simplify = T)[, 1])
  labels sub_label
1    a_1         a
2    b_2         b
3    c_3         c
4    d_4         d

1
map_chr在我提到的例子中更常用,特别是当你需要将它与其他函数一起使用,并且最终需要purrr的映射功能来使所有向量协调地运行时。:) 毫无疑问,这是最简单的解决方案,如果目标只是提取字符,那么可能也是最快的方法。 - GenesRus

5

如果我们想一次性提取多列(当然不需要再次运行拆分),我们可以将GenesRus方法与临时列结合起来,然后在管道中使用负的select()删除它:

library(purrr)
library(dplyr)
library(tibble)
library(stringr)

tibble(labels=c('a_1','b_2','c_3','d_4')) %>% 
  mutate(tmp_chunks = stringr::str_split(labels, stringr::fixed("_"),  n = 2)) %>%
  mutate(sub_label = map_chr(tmp_chunks, 1),
         sub_value = map_chr(tmp_chunks, 2)) %>%
  select(-tmp_chunks)

截至2020年,性能separate()好得多

为了完整起见,值得一提的是

  • map_chr可以使用.default参数(以防某些行中缺少分隔符),
  • 如果需要,也可以通过负数的select()来去除labels

fixed()函数从哪里来?我找不到这个函数。 - GenesRus
1
@GenesRus很棒。这是来自stringr的。更新代码片段。 - DomQ

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