Использование pmap с a для применения разных регулярных выражений к разным переменным в таблице? - PullRequest
0 голосов
/ 30 октября 2018

Этот вопрос очень похож на Использование pmap для применения различных регулярных выражений к разным переменным в таблице? , но отличается, потому что я понял, что моих примеров недостаточно для описания моей проблемы.

Я пытаюсь применить разные регулярные выражения к разным переменным в таблице. Например, я сделал тиббл листинг 1) имя переменной, которую я хочу изменить, 2) регулярное выражение, которое я хочу сопоставить, и 3) строка замены. Я хотел бы применить регулярное выражение / замена к переменной в другом фрейме данных. Обратите внимание, что в целевом тибле могут быть переменные, которые я не хочу изменять, и порядок строк в моем тибле «конфигурации» может не соответствовать порядку столбцов / переменных в моем «целевом» тибле.

Таким образом, моя «конфигурация» может выглядеть так:

test_config <-  dplyr::tibble(
  string_col = c("col1", "col2", "col4", "col3"),
  pattern = c("^\\.$", "^NA$", "^$", "^NULL$"),
  replacement = c("","","", "")
)

Я бы хотел применить это к целевому столу:

test_target <- dplyr::tibble(
  col1 = c("Foo", "bar", ".", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "NA", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", "NULL"),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

Таким образом, цель состоит в том, чтобы заменить другую строку пустой строкой в ​​пользовательском столбце / переменных test_target.

Результат должен быть таким:

result <- dplyr::tibble(
  col1 = c("Foo", "bar", "", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", ""),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

Я могу делать то, что хочу, с циклом for, например:

for (i in seq(nrow(test_config))) {
  test_target <- dplyr::mutate_at(test_target,
                   .vars = dplyr::vars(
                     tidyselect::matches(test_config$string_col[[i]])),
                   .funs = dplyr::funs(
                     stringr::str_replace_all(
                       ., test_config$pattern[[i]], 
                       test_config$replacement[[i]]))
  )
}

Вместо этого, есть ли более аккуратный способ сделать то, что я хочу? До сих пор, думая, что purrr::pmap был инструментом для работы, я создал функцию, которая принимает фрейм данных, имя переменной, регулярное выражение и значение замены и возвращает фрейм данных с одной измененной переменной. Он ведет себя как ожидалось:

testFun <- function(df, colName, regex, repVal){
  colName <- dplyr::enquo(colName)
  df <- dplyr::mutate_at(df,
                         .vars = dplyr::vars(
                           tidyselect::matches(!!colName)),
                         .funs = dplyr::funs(
                           stringr::str_replace_all(., regex, repVal))
  )
}

# try with example
out <- testFun(test_target, 
               test_config$string_col[[1]], 
               test_config$pattern[[1]], 
               "")

Однако, когда я пытаюсь использовать эту функцию с pmap, я сталкиваюсь с парой проблем: 1) есть ли лучший способ построить список для вызова pmap, чем этот?

purrr::pmap(
    list(test_target, 
         test_config$string_col, 
         test_config$pattern, 
         test_config$replacement),
    testFun
)

2) Когда я звоню pmap, я получаю сообщение об ошибке:

Error: Element 2 has length 4, not 1 or 5.

Так что pmap не рад, что я пытаюсь передать тиббл длины 5 как элемент списка, чьи другие элементы имеют длину 4 (я думал, что это переработает тиббл).

Обратите внимание, что ранее, когда я звонил pmap с 4-х строчным тибблом, я получал другую ошибку,

Error in UseMethod("tbl_vars") : 
  no applicable method for 'tbl_vars' applied to an object of class "character"
Called from: tbl_vars(tbl)

Кто-нибудь из вас может предложить способ использовать pmap, чтобы делать то, что я хочу, или существует другой или лучший подход к решению проблемы?

Спасибо!

Ответы [ 4 ]

0 голосов
/ 31 октября 2018

К вашему сведению, результаты бенчмаркинга - предложенный @camille метод "неуклюжий порядок" победил на моем оборудовании!

Unit: milliseconds
          expr       min        lq      mean    median        uq      max neval
          loop 14.808278 16.098818 17.937283 16.811716 20.438360 24.38021    20
 pmap_function  9.486146 10.157526 10.978879 10.628205 11.112485 15.39436    20
     nice_tidy  8.313868  8.633266  9.597485  8.986735  9.870532 14.32946    20
  awkward_tidy  1.535919  1.639706  1.772211  1.712177  1.783465  2.87615    20
    data.table  5.611538  5.652635  8.323122  5.784507  6.359332 51.63031    20
0 голосов
/ 30 октября 2018

Вот два tidyverse способа. Один из них аналогичен ответу data.table в том смысле, что он включает в себя изменение формы данных, объединение их с конфигами и изменение формы на широкую. Другое основано на purrr и, на мой взгляд, немного странно. Я бы порекомендовал первое, так как оно кажется более интуитивным.

Используйте tidyr::gather для придания данным длинной формы, затем dplyr::left_join, чтобы убедиться, что каждое текстовое значение из test_target имеет соответствующий шаблон и замену - даже случаи (col5) без шаблонов будут сохранены с помощью левое соединение.

library(tidyverse)
...

test_target %>%
  gather(key = col, value = text) %>%
  left_join(test_config, by = c("col" = "string_col"))
#> # A tibble: 25 x 4
#>    col   text  pattern replacement
#>    <chr> <chr> <chr>   <chr>      
#>  1 col1  Foo   "^\\.$" ""         
#>  2 col1  bar   "^\\.$" ""         
#>  3 col1  .     "^\\.$" ""         
#>  4 col1  NA    "^\\.$" ""         
#>  5 col1  NULL  "^\\.$" ""         
#>  6 col2  Foo   ^NA$    ""         
#>  7 col2  bar   ^NA$    ""         
#>  8 col2  .     ^NA$    ""         
#>  9 col2  NA    ^NA$    ""         
#> 10 col2  NULL  ^NA$    ""         
#> # ... with 15 more rows

Используя ifelse, замените шаблон, где шаблон существует, или сохраните исходный текст, если шаблон не существует. Оставьте только необходимые шаблоны, добавьте номер строки, потому что spread нужны уникальные идентификаторы, и снова сделайте данные широкими.

test_target %>%
  gather(key = col, value = text) %>%
  left_join(test_config, by = c("col" = "string_col")) %>% 
  mutate(new_text = ifelse(is.na(pattern), text, str_replace(text, pattern, replacement))) %>%
  select(col, new_text) %>%
  group_by(col) %>%
  mutate(row = row_number()) %>%
  spread(key = col, value = new_text) %>%
  select(-row)
#> # A tibble: 5 x 5
#>   col1  col2  col3  col4  col5    
#>   <chr> <chr> <chr> <chr> <chr>   
#> 1 Foo   Foo   Foo   NULL  I       
#> 2 bar   bar   bar   NA    am      
#> 3 ""    .     .     Foo   not     
#> 4 NA    ""    NA    .     changing
#> 5 NULL  NULL  ""    bar   .

Второй способ - сделать крошечный столбик только из имен столбцов, соединить его с конфигами и разбить на список списков. Затем purrr::map2_dfc сопоставляет как созданный вами список, так и столбцы test_target и возвращает фрейм данных с помощью cbind ing. Причина этого заключается в том, что фреймы данных являются технически списками столбцов, поэтому, если вы отображаете фрейм данных, вы рассматриваете каждый столбец как элемент списка. Я не мог заставить ifelse работать прямо здесь - у чего-то в логике были только отдельные строки, возвращающиеся вместо всего вектора.

tibble(all_cols = names(test_target)) %>%
  left_join(test_config, by = c("all_cols" = "string_col")) %>%
  split(.$all_cols) %>%
  map(as.list) %>%
  map2_dfc(test_target, function(info, text) {
    if (is.na(info$pattern)) {
      text
    } else {
      str_replace(text, info$pattern, info$replacement)
    }
  })
#> # A tibble: 5 x 5
#>   col1  col2  col3  col4  col5    
#>   <chr> <chr> <chr> <chr> <chr>   
#> 1 Foo   Foo   Foo   NULL  I       
#> 2 bar   bar   bar   NA    am      
#> 3 ""    .     .     Foo   not     
#> 4 NA    ""    NA    .     changing
#> 5 NULL  NULL  ""    bar   .

Создано в 2018-10-30 представительным пакетом (v0.2.1)

0 голосов
/ 31 октября 2018

Для потомков я также могу выполнить эту задачу, если передам тиббл test_target в pmap_dfr в виде списка (но это не хорошее решение):

purrr::pmap_dfr(
  list(list(test_target),
       test_config$string_col,
       test_config$pattern,
       test_config$replacement),
  testFun
) %>% dplyr::distinct()

Хотя это работает, это не очень хорошее решение, потому что оно перерабатывает элементы списка test_target, фактически делая копии test_target tibble для каждой строки test_config по мере продвижения аргументов, а затем связывает строки итоговые 4 разрыва, чтобы получить большой окончательный вывод (который я отфильтровываю обратно с помощью distinct().

Может быть какой-то способ сделать что-то вроде <<- -подобного подхода, чтобы избежать дублирования целевого тибла, но это еще более странно и плохо.

0 голосов
/ 30 октября 2018

У меня нет опыта с purrr и dplyr, но вот подход с data.table. Подход можно перенести в dplyr, немного погуглив:)

С точки зрения интерпретируемости, подход с циклом, возможно, лучше, поскольку он проще.

edit: внес некоторые изменения в код, в конце концов не использовал purrr

# alternative with data.table
library(data.table)
library(dplyr)

# objects
test_config <-  dplyr::tibble(
  string_col = c("col1", "col2", "col4", "col3"),
  pattern = c("^\\.$", "^NA$", "^$", "^NULL$"),
  replacement = c("","","", "")
)
test_target <- dplyr::tibble(
  col1 = c("Foo", "bar", ".", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "NA", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", "NULL"),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)

multiColStringReplace <- function(test_target, test_config){

  # data.table conversion
  test_target <- as.data.table(test_target)
  test_config <- as.data.table(test_config)

  # adding an id column, as I'm reshaping the data, helps for identification of rows
  # throughout the process
  test_target[,id:=1:.N]

  # wide to long format
  test_target2 <- melt(test_target, id.vars="id")
  head(test_target2)

  # pull in the configuration, can join up on one column now
  test_target2 <- merge(test_target2, test_config, by.x="variable",
                        by.y="string_col", all.x=TRUE)

  # this bit still looks messy to me, haven't used pmap before.
  # I've had to subset the data to the required format, run the pmap with gsub
  # to complete the task, then assign the unlisted vector back in to the original
  # data. Would like to see a better option too!
  test_target2[, result := value]
  test_target2[!is.na(pattern), result := gsub(pattern, replacement, value),
               by = .(id, variable)]

  # case from long to original format, and drop the id
  output <- dcast(test_target2, id~variable,
                  value.var = "result")
  output[, id := NULL]

  # back to tibble
  output <- as_tibble(output)

  return(output)

}

output <- multiColStringReplace(test_target, test_config)
output

result <- dplyr::tibble(
  col1 = c("Foo", "bar", "", "NA", "NULL"),
  col2 = c("Foo", "bar", ".", "", "NULL"),
  col3 = c("Foo", "bar", ".", "NA", ""),
  col4 = c("NULL", "NA", "Foo", ".", "bar"),
  col5 = c("I", "am", "not", "changing", ".")
)
output == result

# compare with old method
old <- test_target
for (i in seq(nrow(test_config))) {
  old <- dplyr::mutate_at(old,
                          .vars = dplyr::vars(
                            tidyselect::matches(test_config$string_col[[i]])),
                          .funs = dplyr::funs(
                            stringr::str_replace_all(
                              ., test_config$pattern[[i]], 
                              test_config$replacement[[i]]))
  )
}
old == result

# speed improves, but complexity rises
microbenchmark::microbenchmark("old" = {
  old <- test_target
  for (i in seq(nrow(test_config))) {
    old <- dplyr::mutate_at(old,
                            .vars = dplyr::vars(
                              tidyselect::matches(test_config$string_col[[i]])),
                            .funs = dplyr::funs(
                              stringr::str_replace_all(
                                ., test_config$pattern[[i]], 
                                test_config$replacement[[i]]))
    )
  }
},
"data.table" = {
  multiColStringReplace(test_target, test_config)
}, times = 20)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...