Быстрый способ преобразовать список векторов символов в список числовых векторов

Я хочу быстро преобразовать список векторов символов в список числовых векторов. Я стараюсь избегать purrr::map(), lapply() и им подобных. Я заканчиваю списком векторов символов из вывода операции stringr. Я хочу использовать Rcpp или внутренний C материал R. Это для пакета filesstrings. Стандартная библиотека C ++ предоставляет stod(), определенный в <string>, но она не ведет себя как as.numeric(), например, она преобразует "12a" в число 12, но мне нравится, как as.numeric() возвращает NA для этого. Вот как я это делаю сейчас.

nums_as_chars <- stringr::str_extract_all(c("a1b2", "c3d4e5", "xyz"), "\\d")
nums_as_chars
#> [[1]]
#> [1] "1" "2"
#> 
#> [[2]]
#> [1] "3" "4" "5"
#> 
#> [[3]]
#> character(0)
nums <- purrr::map(nums_as_chars, as.numeric)
nums
#> [[1]]
#> [1] 1 2
#> 
#> [[2]]
#> [1] 3 4 5
#> 
#> [[3]]
#> numeric(0)

Создано 4 апреля 2018 г. пакетом REPEX (v0.2.0).


person Rory Nolan    schedule 04.04.2018    source источник
comment
Правильно, исходя из ответа @roland, вам действительно нужно поддерживать структуру списка?   -  person rmflight    schedule 04.04.2018
comment
И да, было бы полезно больше данных, которые дадут результаты, полезные для профилирующих решений.   -  person rmflight    schedule 04.04.2018
comment
Мне нужно сохранить список. Вот почему я указываю список векторов символов для списка числовых векторов.   -  person Rory Nolan    schedule 05.04.2018
comment
Я доволен вашим значением length-3000 character_vector в качестве тестового набора данных.   -  person Rory Nolan    schedule 05.04.2018


Ответы (5)


Хорошо, поэтому я создал более реалистичный пример с 3000 строками для профилирования и пробовал все возможные способы сделать это.

character_vector <- rep(c("a1b2", "c3d4e5", "xyz"), 1000)


try_1 <- function(chars){
  extracted_numbers <- stringr::str_extract_all(chars, "\\d")
  lapply(extracted_numbers, as.numeric)
}

try_2 <- function(chars){
  extracted_numbers <- stringr::str_extract_all(chars, "\\d")
  purrr::map(extracted_numbers, as.numeric)
}

try_3 <- function(chars){
  extracted_numbers <- stringr::str_extract_all(chars, "\\d")
  relist(as.numeric(unlist(extracted_numbers)), extracted_numbers)
}

try_4 <- function(chars){
  convert_fun <- function(x){as.numeric(stringr::str_extract_all(x, "\\d")[[1]])}
  lapply(chars, convert_fun)
}

# if you don't need to keep the list ...
try_5 <- function(chars){
  extracted_numbers <- stringr::str_extract_all(chars, "\\d")
  suppressWarnings(as.numeric(unlist(extracted_numbers)))
}


microbenchmark::microbenchmark(try_1(character_vector),
                               try_2(character_vector),
                               try_3(character_vector),
                               try_4(character_vector),
                               try_5(character_vector))
#> Unit: milliseconds
#>                     expr        min         lq       mean     median
#>  try_1(character_vector)   2.701769   2.866486   3.304917   3.005177
#>  try_2(character_vector)   3.936557   4.295735   4.872892   4.391737
#>  try_3(character_vector)  12.441844  13.317455  15.759840  14.250013
#>  try_4(character_vector) 183.180143 187.789907 191.298661 190.073565
#>  try_5(character_vector)   1.846848   1.964761   2.090801   2.026860
#>          uq        max neval
#>    3.275250  10.569255   100
#>    4.726425  17.007687   100
#>   16.995679  49.983457   100
#>  193.012754 215.532544   100
#>    2.105214   4.396379   100

Обратите внимание, что единицы измерения - миллисекунды, и для 3000 записей потребовалось lapply 3 миллисекунды, чтобы выполнить список из 3000. Мне это не кажется таким уж необоснованным.

Решение purrr::map было очень близко к lapply, затем решение @roland было длиннее, и тогда моя первая мысль была на много хуже. Если вас не волнует структура list (что, как я полагаю, вы делаете), то вы можете сократить время до 2 миллисекунд.

person rmflight    schedule 04.04.2018
comment
Почему вы включаете тайминги регулярных выражений во все попытки, кроме последней? - person Roland; 04.04.2018
comment
потому что я идиот ...., и я не уверен, что этот код действительно действителен и работает банкомат - person rmflight; 04.04.2018

Вы не предоставляете ничего подходящего для разумных тестов. Итак, проверьте это на себе:

relist(as.numeric(unlist(nums_as_chars)),
       nums_as_chars)
#[[1]]
#[1] 1 2
#
#[[2]]
#[1] 3 4 5
#
#[[3]]
#numeric(0)
person Roland    schedule 04.04.2018

Стандартная библиотека C ++ предоставляет stod(), определенный в <string>, но она не ведет себя как as.numeric(), например конвертирует "12a" в число 12, но мне нравится, как as.numeric() возвращает NA для этого.

Эта часть легко решается: просто используйте второй параметр функции чтобы убедиться, что ввод был использован:

Rcpp::NumericVector as_numeric(std::string const& str) {
    std::size_t pos;
    double value = std::stod(&str[0], &pos);
    return NumericVector::create(pos == str.size() ? value : NA_REAL);
}
〉 as_numeric('12')
[1] 12

〉 as_numeric('12a')
[1] NA

… Очевидно, это должно быть векторизовано для повышения производительности.

person Konrad Rudolph    schedule 04.04.2018
comment
Это здорово, это то, что я искал. Очевидно, я неправильно понял stod документы. - person Rory Nolan; 05.04.2018

Вдохновленный ответом @Konrad, я закодировал следующее, используя Rcpp.

NumericVector char_to_num(CharacterVector x) {
  std::size_t n = x.size();
  if (n == 0) return NumericVector(0);
  NumericVector out(n);
  for (std::size_t i = 0; i != n; ++i) {
    std::string x_i(x[i]);
    double number = NA_REAL;
    try {
      std::size_t pos;
      number = std::stod(x_i, &pos);
      number = ((pos == x_i.size()) ? number : NA_REAL);
    } catch (const std::invalid_argument& e) {
      ;  // do nothing
    }
    out[i] = number;
  }
  return out;
}

// [[Rcpp::export]]
List lst_char_to_num(List x) {
  std::size_t n = x.size();
  List out(n);
  for (std::size_t i = 0; i != n; ++i)
    out[i] = char_to_num(x[i]);
  return out;
}

Этот lst_char_to_num() оказывается лучшим ответом. Я сравниваю его с моими любимыми ответами на данный момент: try1, try2 и try3 от @rmflight. try1 был самым быстрым до сих пор (на большом наборе данных, о чем я беспокоюсь). Я убрал операцию stringr из таймингов, потому что хочу чисто оценить скорость преобразования списка.

character_vector <- rep(c("a1b2", "c3d4e5", "xyz"), 1000)
extracted_numbers <- stringr::str_extract_all(character_vector, "\\d")

try_1 <- function(char_list) {
  lapply(char_list, as.numeric)
}

try_2 <- function(char_list) {
  purrr::map(char_list, as.numeric)
}

try_3 <- function(char_list) {
  relist(as.numeric(unlist(char_list)), char_list)
}

microbenchmark::microbenchmark(try_1(extracted_numbers),
                               try_2(extracted_numbers),
                               try_3(extracted_numbers),
                               lst_char_to_num(extracted_numbers),
                               times = 1000)

Unit: microseconds
                              expr       min         lq       mean     median        uq        max neval  cld
          try_1(extracted_numbers)  1068.823  1334.9060  1518.7589  1477.7825  1559.791   5318.318  1000  b  
          try_2(extracted_numbers)  2029.832  2581.6655  2974.4126  2856.8560  3057.930   9846.862  1000   c 
          try_3(extracted_numbers) 10015.929 12261.6405 14043.5922 13188.8465 14802.795 165217.152  1000    d
lst_char_to_num(extracted_numbers)   500.858   681.5895   827.5021   765.9505   830.311   6744.985  1000 a   
person Rory Nolan    schedule 05.04.2018

Вот базовое решение, быстрее для небольшого примера, но медленнее для расширенного теста @ rmflight:

mm <- function(chars){
  lapply(strsplit(chars,"[a-z]"),function(x) as.numeric(x[x!=""]))
}

character_vector <- c("a1b2", "c3d4e5", "xyz")

mm(character_vector)
# [[1]]
# [1] 1 2
# 
# [[2]]
# [1] 3 4 5
# 
# [[3]]
# numeric(0)

microbenchmark::microbenchmark(try_1(character_vector),
                               try_2(character_vector),
                               try_3(character_vector),
                               try_4(character_vector),
                               try_5(character_vector),
                               mm(character_vector))

# Unit: microseconds
#                    expr     min       lq      mean   median       uq      max neval
# try_1(character_vector)  74.007  83.0690  96.18563  93.2630 102.1365  158.962   100
# try_2(character_vector) 375.694 409.4875 455.99043 444.7915 489.5345  853.335   100
# try_3(character_vector)  97.416 110.6315 128.83893 128.5670 141.9715  218.620   100
# try_4(character_vector) 211.823 229.7590 251.82474 244.6740 257.5115  412.696   100
# try_5(character_vector)  71.363  94.0180 121.96305 113.4635 141.4050  255.245   100
#    mm(character_vector)  14.726  23.0330  62.70177  26.4315  29.2630 3652.345   100

character_vector <- rep(c("a1b2", "c3d4e5", "xyz"), 1000)

microbenchmark::microbenchmark(try_1(character_vector),
                               try_2(character_vector),
                               try_3(character_vector),
                               try_4(character_vector),
                               try_5(character_vector),
                               mm(character_vector))

# Unit: microseconds
#                    expr        min         lq       mean      median         uq        max neval
# try_1(character_vector)   3286.091   3466.764   4080.628   3579.2825   3836.604  30358.295   100
# try_2(character_vector)   5013.525   5290.859   6081.001   5526.0920   5878.942  32019.653   100
# try_3(character_vector)  18456.932  19466.395  22987.824  20040.6965  21318.998  65432.958   100
# try_4(character_vector) 215098.270 224078.287 245975.607 237691.9815 259353.446 356781.135   100
# try_5(character_vector)    906.951   1044.013   1106.771   1063.8365   1105.559   1649.276   100
#    mm(character_vector)   7550.495   7872.383   9204.357   8215.6040   8733.268  36034.853   100

К вашему сведению, другое решение, но медленнее:

mm2 <- function(chars){
  lapply(gsub("[a-z]"," ",vec),function(x) scan(text=x,what=double(),quiet=TRUE))
}

mm2(character_vector)
# [[1]]
# [1] 1 2
# 
# [[2]]
# [1] 3 4 5
# 
# [[3]]
# numeric(0)
person Moody_Mudskipper    schedule 04.04.2018
comment
FYI, выполняя 100 повторений mm на векторе 3000 tho, дает мне значение 7000 микросекунд, что в 3 раза хуже, чем try_1 - person rmflight; 04.04.2018
comment
Хорошо, я наивно просто запустил пример, извините, я отредактирую. Возможно, исправьте форматирование вашего сообщения, чтобы определения character_vector были частью серого отформатированного фрейма. - person Moody_Mudskipper; 04.04.2018
comment
Упс, мне очень жаль. Пропустил возврат, когда делал правку, и недостаточно хорошо вычитал корректуру. - person rmflight; 04.04.2018