创建新列以指示列名在另一个字符串向量中的位置(使用 dplyr、purrr 和 stringr)

Create new columns to indicate column name's position inside another string vector (with dplyr, purrr, and stringr)

鉴于此示例数据:

require(stringr)
require(tidyverse)

labels <- c("foo", "bar", "baz")
n_rows <- 4

df <- 1:n_rows %>%
  map(~ data.frame(
      block_order=paste(sample(labels, size=length(labels), replace=FALSE),
                        collapse="|"))) %>%
  bind_rows()

df
  block_order
1 foo|bar|baz
2 baz|bar|foo
3 foo|baz|bar
4 foo|bar|baz

我想为 labels 中的每个字符串生成一列,它采用该字符串在每一行中以 | 分隔的序列中的位置值。

期望的输出:

  block_order foo bar baz
1 foo|bar|baz   1   2   3
2 baz|bar|foo   3   2   1
3 foo|baz|bar   1   3   2
4 foo|bar|baz   1   2   3

我一直在 dplyr/purrr 设置中尝试不同的变体,就像这个例子,我在 label 的每个值中 map,然后尝试在 str_split:

上使用 match 获得它在 block_order 中的位置
labels %>%
  map(~ df %>%
        transmute(!!.x := match(!!.x, str_split(block_order, 
                                                "\|", 
                                                simplify=TRUE)))) %>%
  bind_cols(df, .)

但这会产生意外的输出:

  block_order foo bar baz
1 foo|bar|baz   1   5   2
2 baz|bar|foo   1   5   2
3 foo|baz|bar   1   5   2
4 foo|bar|baz   1   5   2

我不太确定这些数字代表什么,或者为什么它们都一样。

如果有人能帮我弄清楚 (a) 如何在 dplyr/purrr 框架中实现我想要的输出,以及 (b) 为什么这里提出的解决方案给出了它的输出,我将不胜感激。

我们可以将 'block_order' 拆分为 |,使用 lapply 遍历 vectorlist,使用 [=17 获取索引=], rbind vector 并将其分配给创建新列

labels <- c("foo", "bar", "baz")
df[labels] <- do.call(rbind, lapply(strsplit(df$block_order, "|",
         fixed = TRUE), match, table = labels))

或与 tidyverse

类似的想法
library(tidyverse)
str_split(df$block_order, "[|]") %>%
       map(~ .x %>% 
              match(table= labels)) %>% 
      do.call(rbind, .) %>% 
      as_tibble %>% 
      set_names(labels) %>%
      bind_cols(df, .)
#   block_order foo bar baz
#1 foo|bar|baz   1   2   3
#2 baz|bar|foo   3   2   1
#3 foo|baz|bar   1   3   2
#4 foo|bar|baz   1   2   3

另一种选择是使用 separate_rows,将其重塑为 'long' 格式,然后 spread 返回

rownames_to_column(df, 'rn') %>%
    separate_rows(block_order) %>% 
    group_by(rn) %>% 
    mutate(ind = match(block_order, labels), labels = factor(labels, levels = labels)) %>%
    select(-block_order) %>%
    spread(labels, ind) %>% 
    ungroup %>%
    select(-rn) %>% 
    bind_cols(df, .)

我认为这可能有效:

library(tidyr)
library(purrr)
position_counter <- function(...) {
  row = list(...)
  row %>% map(~which(row == .)) %>% setNames(row)
}

df %>%
  separate(block_order, labels) %>% 
  pmap_df(position_counter)

除非您出于其他原因需要,如果您只是为 labels 的每个值确定第一个匹配项的位置,则不必完全拆分字符串,regexpr 将给你。 mapping labels 将为 labels 中的每个字符串提供一个包含一个元素的列表(因此这是一个快速迭代),然后您可以 pmap rank过来获取指数。使用 *_dfr 版本将结果简化为数据框并绑定到原始数​​据,

library(tidyverse)
set.seed(47)

labels <- c("foo", "bar", "baz")
df <- data_frame(block_order = replicate(10, paste(sample(labels), collapse = "|")))

labels %>% 
    map(~regexpr(.x, df$block_order)) %>% 
    pmap_dfr(~set_names(as.list(rank(c(...))), labels)) %>% 
    bind_cols(df, .)
#> # A tibble: 10 x 4
#>    block_order   foo   bar   baz
#>    <chr>       <dbl> <dbl> <dbl>
#>  1 baz|foo|bar    2.    3.    1.
#>  2 baz|bar|foo    3.    2.    1.
#>  3 bar|foo|baz    2.    1.    3.
#>  4 baz|foo|bar    2.    3.    1.
#>  5 foo|bar|baz    1.    2.    3.
#>  6 baz|foo|bar    2.    3.    1.
#>  7 foo|baz|bar    1.    3.    2.
#>  8 bar|baz|foo    3.    1.    2.
#>  9 baz|foo|bar    2.    3.    1.
#> 10 foo|bar|baz    1.    2.    3.

如果您更喜欢 stringr/stringi 而不是基础正则表达式,您可以通过在相同的安排中将 regexpr 调用更改为 str_locate(df$block_order, .x)[, "start"]stringi::stri_locate_first_fixed 来实现相同的目的。