Como classificar eficientemente os caracteres em uma string em R?

9

Como posso classificar eficientemente os caracteres de cada string em um vetor? Por exemplo, dado um vetor de strings:

set.seed(1)
strings <- c(do.call(paste0, replicate(4, sample(LETTERS, 10000, TRUE), FALSE)),
do.call(paste0, replicate(3, sample(LETTERS, 10000, TRUE), FALSE)),
do.call(paste0, replicate(2, sample(LETTERS, 10000, TRUE), FALSE)))

Eu escrevi uma função que irá dividir cada seqüência de caracteres em um vetor, classificar o vetor e recolher a saída:

sort_cat <- function(strings){
  tmp <- strsplit(strings, split="")
  tmp <- lapply(tmp, sort)
  tmp <- lapply(tmp, paste0, collapse = "")
  tmp <- unlist(tmp)
  return(tmp)
}
sorted_strings <- sort_cat(strings)

No entanto, o vetor de strings ao qual preciso aplicar isso é muito longo e essa função é muito lenta. Alguém tem alguma sugestão de como melhorar o desempenho?

Powege
fonte
11
Confira o pacote stringi - ele oferece uma aceleração versus base. A resposta de Rich Scriven fornece mais detalhes: stackoverflow.com/questions/5904797/… #
user2474226:
O lettersnão são sempre de comprimento três como no seu exemplo, são eles?
Java.sf # 8/19
Não, o comprimento das strings pode variar.
Powege 8/08/19
Eu acho que a adição fixed = TRUEde strsplit()pode melhorar o desempenho, pois não irá envolver o uso de regex.
tmfmnk

Respostas:

3

Você pode reduzir o tempo minimizando o número de loops, com certeza, e ainda usando o parallelpacote ... minha abordagem seria dividir seqüências de caracteres uma vez e, em seguida, classificar e colar o loop:

sort_cat <- function(strings){
    tmp <- strsplit(strings, split="")
    tmp <- lapply(tmp, sort)
    tmp <- lapply(tmp, paste0, collapse = "")
    tmp <- unlist(tmp)
    return(tmp)
}

sort_cat2 <- function(strings){
    unlist(mcMap(function(i){
        stri_join(sort(i), collapse = "")
    }, stri_split_regex(strings, "|", omit_empty = TRUE, simplify = F), mc.cores = 8L))
}

> microbenchmark::microbenchmark(
+     old = sort_cat(strings[1:500000]),
+     new = sort_cat2(strings[1:500000]),
+     times = 1
+ )
Unit: seconds
 expr        min         lq       mean     median         uq        max neval
  old 9.62673395 9.62673395 9.62673395 9.62673395 9.62673395 9.62673395     1
  new 5.10547437 5.10547437 5.10547437 5.10547437 5.10547437 5.10547437     1

Raspa-se como 4 segundos, mas ainda não é tão rápido ...

Editar

Ok, consegui resolver o problema usando apply.. estratégia aqui:

1) extrair letras em vez de dividir limites 2) criar uma matriz com os resultados 3) iterar por linhas 4) Classificar 5) Unir

Você evita vários loops e cancelamentos de lista. ... IGNORE:? Ressalva é que, se as cordas tiverem comprimentos diferentes, será necessário remover qualquer NA vazia ou NA apply, comoi[!is.na(i) && nchar(i) > 0]

sort_cat3 <- function(strings){
    apply(stri_extract_all_regex(strings, "\\p{L}", simplify = TRUE), 1, function(i){
        stri_join(stri_sort(i), collapse = "")
    })
}

> microbenchmark::microbenchmark(
+     old = sort_cat(strings[1:500000]),
+     mapping = sort_cat2(strings[1:500000]),
+     applying = sort_cat3(strings[1:500000]),
+     times = 1
+ )
Unit: seconds
     expr         min          lq        mean      median          uq         max neval
      old 10.35101934 10.35101934 10.35101934 10.35101934 10.35101934 10.35101934     1
  mapping  5.12771799  5.12771799  5.12771799  5.12771799  5.12771799  5.12771799     1
 applying  3.97775326  3.97775326  3.97775326  3.97775326  3.97775326  3.97775326     1

Leva-nos de 10,3 segundos para 3,98

Carl Boneri
fonte
Qual é a aceleração se você executar a função original em paralelo?
slava-kohut
reduzido em pouco mais de 50%. tmp <- strsplit(strings, split="") unlist(mclapply(tmp, function(i){ paste0(sort(i), collapse = "") }))
Carl Boneri 8/10/19
@Gregor faz. Apenas testado e parece?
24519 Carl Boneri
Cool, apenas verificando :)
Gregor Thomas
Não, de maneira alguma .. totalmente eu mesma tive a mesma pergunta .. o que significa omitir a nota que coloquei na resposta sobre a remoção de NA / vazia ... não precisa dela. stringié meu pacote favorito pelo homem agora ...
Carl Boneri
4

A reimplementação usando stringifornece uma aceleração de aproximadamente 4x. Também editei sort_catpara usar fixed = TRUEno strsplit, o que o torna um pouco mais rápido. E obrigado a Carl pela sugestão de loop único, que nos acelera um pouco mais.

sort_cat <- function(strings){
  tmp <- strsplit(strings, split="", fixed = TRUE)
  tmp <- lapply(tmp, sort)
  tmp <- lapply(tmp, paste0, collapse = "")
  tmp <- unlist(tmp)
  return(tmp)
}

library(stringi)
sort_stringi = function(s) {
  s = stri_split_boundaries(s, type = "character")
  s = lapply(s, stri_sort)
  s = lapply(s, stri_join, collapse = "")
  unlist(s)
}

sort_stringi_loop = function(s) {
  s = stri_split_boundaries(s, type = "character")
  for (i in seq_along(s)) {
    s[[i]] = stri_join(stri_sort(s[[i]]), collapse = "")
  }
  unlist(s)
}

bench::mark(
  sort_cat(strings),
  sort_stringi(strings),
  sort_stringi_loop(strings)
)
# # A tibble: 3 x 13
#   expression                    min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory
#   <bch:expr>                 <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list>
# 1 sort_cat(strings)          23.01s 23.01s    0.0435    31.2MB     2.17     1    50     23.01s <chr ~ <Rpro~
# 2 sort_stringi(strings)       6.16s  6.16s    0.162     30.5MB     2.11     1    13      6.16s <chr ~ <Rpro~
# 3 sort_stringi_loop(strings)  5.75s  5.75s    0.174     15.3MB     1.74     1    10      5.75s <chr ~ <Rpro~
# # ... with 2 more variables: time <list>, gc <list>

Este método também pode ser usado em paralelo. Criar um perfil do código para ver quais operações realmente demoram mais seria um bom próximo passo, se você quiser ir ainda mais rápido.

Gregor Thomas
fonte
11
Eu acho que isso vai acabar mais rápido do que se aplica e não depende da remoção de valores vazios, se diferentes comprimentos. pode sugerir um loop envolto em unlist, embora?
Carl Boneri 8/10/19
11
O loop único melhora a velocidade um pouco mais, obrigado!
Gregor Thomas
sim cara. isso ainda está me incomodando, no entanto. Eu me sinto como im faltando uma maneira muito óbvia e mais fácil de fazer essa coisa toda ....
Carl Boneri
Quero dizer, provavelmente seria muito fácil escrever uma função RCPP que apenas faça isso e seria extremamente rápida. Mas trabalhando no R, acho que estamos limitados a basicamente executar essas etapas.
Gregor Thomas
Isso é o que eu estava pensando: C ++
Carl Boneri
1

Esta versão é um pouco mais rápida

sort_cat2=function(strings){
A=matrix(unlist(strsplit(strings,split="")),ncol=3,byrow=TRUE)
B=t(apply(A,1,sort))
paste0(B[,1],B[,2],B[,3])
}

Mas acho que pode ser otimizado

Félix Cuneo
fonte
Só funcionará se o comprimento de todas as strings for o mesmo. Bom e rápido, no entanto!
Gregor Thomas