将数据框字符串列拆分为多个列

334

我想要获取这种形式的数据

before = data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))
  attr          type
1    1   foo_and_bar
2   30 foo_and_bar_2
3    4   foo_and_bar
4    6 foo_and_bar_2

然后在上述的"type"列上使用split()方法,以得到类似以下的内容:

  attr type_1 type_2
1    1    foo    bar
2   30    foo  bar_2
3    4    foo    bar
4    6    foo  bar_2

我曾经想到了一些涉及apply的非常复杂的东西,但是现在我已经找不到了。它似乎太复杂了,不应该是最好的方法。我可以像下面这样使用strsplit,但是不清楚如何将其返回到数据框中的2列中。

> strsplit(as.character(before$type),'_and_')
[[1]]
[1] "foo" "bar"

[[2]]
[1] "foo"   "bar_2"

[[3]]
[1] "foo" "bar"

[[4]]
[1] "foo"   "bar_2"

感谢任何指点。我还没有完全掌握 R 列表。

18个回答

355

使用 stringr::str_split_fixed

library(stringr)
str_split_fixed(before$type, "_and_", 2)

4
今天这对我的问题非常有效,但它在每行开头添加了一个“c”。你有任何想法为什么会这样吗?left_right <- str_split_fixed(as.character(split_df),'\">',2) - LearneR
我想使用带有“…”的模式进行分割,但当我应用该函数时,它返回空值。可能出了什么问题。我的类型类似于“test...score”。 - user3841581
6
我知道这是你以前的问题,但是文档中已经涵盖了这一点 - str_split_fixed("aaa...bbb", fixed("..."), 2)pattern=参数中使用fixed()来“匹配一个固定的字符串”可行。在正则表达式中,“.”代表任何字符。 - thelatemail
虽然这不是直接的问题,但你可以使用以下|来搜索几个字符串。例如,对于_and__or_,代码应该是:str_split_fixed(before$type, c("_and_|_or_"), 2) - Martin
我觉得上面@Martin的评论解释了我的问题。我需要用"|"来分割我的字段。直到我添加了fixed("|"),接受的答案对我来说才起作用。 - undefined
显示剩余3条评论

269
你可以使用tidyr包。
before <- data.frame(
  attr = c(1, 30 ,4 ,6 ), 
  type = c('foo_and_bar', 'foo_and_bar_2')
)

library(tidyr)
before |>
  separate_wider_delim(type, delim = "_and_", names = c("foo", "bar"))
# # A tibble: 4 × 3
#    attr foo   bar  
#   <dbl> <chr> <chr>
# 1     1 foo   bar  
# 2    30 foo   bar_2
# 3     4 foo   bar  
# 4     6 foo   bar_2

(或者使用较旧版本的 tidyr
before %>%
  separate(type, c("foo", "bar"), "_and_")

##   attr foo   bar
## 1    1 foo   bar
## 2   30 foo bar_2
## 3    4 foo   bar
## 4    6 foo bar_2

4
使用 separate 函数时,是否有办法限制分割的次数?比如说,我只想在第一个 '_' 处进行一次分割(或者使用 str_split_fixed 并将结果添加到现有数据框中)? - JelenaČuklina
@hadley 如果我想基于第二个“_”拆分怎么办?我希望得到的值是foo_andbar/bar_2 - Prradep
2
tidyr::separate已被tidyr::separate_wider_delim取代。 - robertspierre

94

5年后添加了强制性的data.table解决方案

library(data.table) ## v 1.9.6+ 
setDT(before)[, paste0("type", 1:2) := tstrsplit(type, "_and_")]
before
#    attr          type type1 type2
# 1:    1   foo_and_bar   foo   bar
# 2:   30 foo_and_bar_2   foo bar_2
# 3:    4   foo_and_bar   foo   bar
# 4:    6 foo_and_bar_2   foo bar_2

我们还可以通过添加 type.convertfixed 参数来确保生成的列具有正确的类型 并且 提高性能(因为“_and_”实际上不是一个正则表达式)。

setDT(before)[, paste0("type", 1:2) := tstrsplit(type, "_and_", type.convert = TRUE, fixed = TRUE)]

1
如果您的'_and_'模式数量不同,您可以使用max(lengths(strsplit(before$type, '_and_'))) 来查找最大匹配数(即未来列数)。 - andschar
这是我最喜欢的答案,非常有效!您能否解释一下它是如何工作的?为什么要转置(strsplit(...))而不是使用paste0来连接字符串-而不是拆分它们... - Gecko
3
@Gecko,我不确定问题是什么。如果只使用 strsplit,它会创建一个单矢量,在每个位置上有两个值,所以 tstrsplit 将其转置为两个矢量,每个矢量中只有一个值。paste0 仅用于创建列名,不用于值。在等式的左侧是列名,在右侧是对该列进行分割和转置操作。:= 代表“现场分配”,因此您看不到 <- 分配运算符。 - David Arenburg

74

另一种方法:使用rbindout上:

before <- data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))  
out <- strsplit(as.character(before$type),'_and_') 
do.call(rbind, out)

     [,1]  [,2]   
[1,] "foo" "bar"  
[2,] "foo" "bar_2"
[3,] "foo" "bar"  
[4,] "foo" "bar_2"

并且要结合:

data.frame(before$attr, do.call(rbind, out))

7
在更新的 R 版本中,另一个选择是 strcapture("(.*)_and_(.*)", as.character(before$type), data.frame(type_1 = "", type_2 = ""))。该函数可以将输入字符串按照指定的正则表达式进行拆分,并返回一个数据框,其中包含按组分隔的子字符串。 - alexis_laz
这是正确的解决方案。简单且不需要第三方包。 - Cole

46

请注意,sapply与"["一起使用可以用于提取这些列表中的第一个或第二个项目:

before$type_1 <- sapply(strsplit(as.character(before$type),'_and_'), "[", 1)
before$type_2 <- sapply(strsplit(as.character(before$type),'_and_'), "[", 2)
before$type <- NULL

这里还有一个 gsub 方法:

before$type_1 <- gsub("_and_.+$", "", before$type)
before$type_2 <- gsub("^.+_and_", "", before$type)
before$type <- NULL

35

这里有一行代码,与aniko的解决方案类似,但使用了hadley的stringr包:

do.call(rbind, str_split(before$type, '_and_'))

1
不错的发现,对我来说是最好的解决方案。虽然比使用stringr包慢了一点。 - Melka
这个函数被重命名为 strsplit() 了吗? - user5359531

31

除了这些选项之外,您还可以像这样使用我的splitstackshape :: cSplit 函数:

library(splitstackshape)
cSplit(before, "type", "_and_")
#    attr type_1 type_2
# 1:    1    foo    bar
# 2:   30    foo  bar_2
# 3:    4    foo    bar
# 4:    6    foo  bar_2

1
三年后 - 这个选项对我遇到的类似问题效果最好 - 但是我正在处理的数据框有54列,我需要将它们全部分成两部分。除了逐个输入上述命令之外,是否有其他方法可以做到这一点?非常感谢,Nicki。 - Nicki
@Nicki,你尝试过提供列名或列位置的向量吗?那应该可以解决问题... - A5C1D2H2I1M1N2O1R2T1
不仅仅是重命名列 - 我需要按照上述方式将列拆分,从而有效地将我的数据框中的列数加倍。最终我使用了以下代码:df2 <- cSplit(df1, splitCols = 1:54, "/") - Nicki

30

这个主题已经被讨论得差不多了,但我想提供一个稍微更一般化的解决方案,其中你不知道输出列的数量。例如,你有以下内容:

before = data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2', 'foo_and_bar_2_and_bar_3', 'foo_and_bar'))
  attr                    type
1    1             foo_and_bar
2   30           foo_and_bar_2
3    4 foo_and_bar_2_and_bar_3
4    6             foo_and_bar

我们无法使用dplyr的separate()函数,因为在拆分之前我们不知道结果列的数量,所以我创建了一个函数,使用stringr来拆分列,给定模式和生成列的名称前缀。我希望使用的编码模式是正确的。

split_into_multiple <- function(column, pattern = ", ", into_prefix){
  cols <- str_split_fixed(column, pattern, n = Inf)
  # Sub out the ""'s returned by filling the matrix to the right, with NAs which are useful
  cols[which(cols == "")] <- NA
  cols <- as.tibble(cols)
  # name the 'cols' tibble as 'into_prefix_1', 'into_prefix_2', ..., 'into_prefix_m' 
  # where m = # columns of 'cols'
  m <- dim(cols)[2]

  names(cols) <- paste(into_prefix, 1:m, sep = "_")
  return(cols)
}

我们可以在dplyr管道中使用split_into_multiple,如下所示:
after <- before %>% 
  bind_cols(split_into_multiple(.$type, "_and_", "type")) %>% 
  # selecting those that start with 'type_' will remove the original 'type' column
  select(attr, starts_with("type_"))

>after
  attr type_1 type_2 type_3
1    1    foo    bar   <NA>
2   30    foo  bar_2   <NA>
3    4    foo  bar_2  bar_3
4    6    foo    bar   <NA>

然后我们可以使用gather来整理...

after %>% 
  gather(key, val, -attr, na.rm = T)

   attr    key   val
1     1 type_1   foo
2    30 type_1   foo
3     4 type_1   foo
4     6 type_1   foo
5     1 type_2   bar
6    30 type_2 bar_2
7     4 type_2 bar_2
8     6 type_2   bar
11    4 type_3 bar_3

1
这非常有用。多年后,我在想是否可以引入可以插入for循环中的列名。 例如,我想将10列(或更多)拆分成多个列,并且我不想逐个拆分它们。我希望拆分后的结果列能够绑定在一起。我找不到以编程方式选择列名的方法(它会给我错误)。选项!!as.name()说没有这样的属性......你会怎么做? - MEC
很高兴你仍然觉得这很有用,@MEC。我的R知识已经荒废了,不知道该如何提示你这一部分。我认为Python的pandas可能会有所帮助,但需要一些功课... - Yannis P.

18

简单的方法是使用 sapply()[ 函数:

before <- data.frame(attr = c(1,30,4,6), type=c('foo_and_bar','foo_and_bar_2'))
out <- strsplit(as.character(before$type),'_and_')

例如:

> data.frame(t(sapply(out, `[`)))
   X1    X2
1 foo   bar
2 foo bar_2
3 foo   bar
4 foo bar_2

sapply()函数的结果是一个矩阵,需要转置并重新转换为数据框。然后进行一些简单的操作即可得到所需的结果:

after <- with(before, data.frame(attr = attr))
after <- cbind(after, data.frame(t(sapply(out, `[`))))
names(after)[2:3] <- paste("type", 1:2, sep = "_")

此时,after 就是你想要的东西。

> after
  attr type_1 type_2
1    1    foo    bar
2   30    foo  bar_2
3    4    foo    bar
4    6    foo  bar_2

11
自R版本3.4.0起,您可以使用utils包中的strcapture()函数(已包含在基本的R安装程序中),将输出绑定到其他列。
out <- strcapture(
    "(.*)_and_(.*)",
    as.character(before$type),
    data.frame(type_1 = character(), type_2 = character())
)

cbind(before["attr"], out)
#   attr type_1 type_2
# 1    1    foo    bar
# 2   30    foo  bar_2
# 3    4    foo    bar
# 4    6    foo  bar_2

1
这是最为一致的“专为此目的而建”。 - Chris

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