¿Cómo ordenar eficientemente los caracteres en una cadena en R?

9

¿Cómo puedo ordenar eficientemente los caracteres de cada cadena en un vector? Por ejemplo, dado un vector de cadenas:

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)))

He escrito una función que dividirá cada cadena en un vector, clasificará el vector y luego colapsará la salida:

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)

Sin embargo, el vector de cadenas al que necesito aplicar esto es muy largo y esta función es demasiado lenta. ¿Alguien tiene alguna sugerencia sobre cómo mejorar el rendimiento?

Powege
fuente
1
Echa un vistazo al paquete stringi: ofrece una aceleración vs base. La respuesta de Rich Scriven da más detalles: stackoverflow.com/questions/5904797/…
user2474226
El lettersno siempre son de la longitud de tres como en su ejemplo, ¿verdad?
jay.sf
No, la longitud de las cuerdas puede variar.
Powege
Creo que la adición fixed = TRUEde strsplit()puede mejorar el rendimiento, ya que no implica el uso de expresiones regulares.
tmfmnk

Respuestas:

3

Puede reducir el tiempo minimizando la cantidad de bucles con seguridad, y hacerlo aún más utilizando el parallelpaquete ... mi enfoque sería dividir cadenas una vez, luego en el bucle ordenar y pegar:

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

Se afeita como 4 segundos, pero aún no es tan rápido ...

Editar

Bien, lo conseguí usando la applyestrategia aquí:

1) extraer letras en lugar de dividir límites 2) crear una matriz con los resultados 3) iterar por filas 4) Ordenar 5) Unir

A evitar múltiples bucles y unlisting .... IGNORE: ? Salvedad es si cadenas diferentes longitudes, tendrá que eliminar cualquier vacío o NA dentro de la applytales 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

Nos lleva de 10.3 segundos a 3.98

Carl Boneri
fuente
¿Cuál es la aceleración si ejecuta la función original en paralelo?
slava-kohut
derribado por un poco más del 50%. tmp <- strsplit(strings, split="") unlist(mclapply(tmp, function(i){ paste0(sort(i), collapse = "") }))
Carl Boneri
@ Gregor lo hace. Acabo de probar y parece?
Carl Boneri
Genial, solo comprobando :)
Gregor Thomas
No, en absoluto ... totalmente tuve la misma pregunta ... lo que significa omitir la nota que puse en la respuesta sobre la eliminación de NA / vacío ... no lo necesito. stringies mi paquete favorito de lejos hombre ...
Carl Boneri
4

La reimplementación usando stringiproporciona una aceleración de aproximadamente 4x. También edité sort_catpara usar fixed = TRUEen el strsplit, lo que lo hace un poco más rápido. Y gracias a Carl por la sugerencia de bucle único, que nos acelera un poco más.

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 también podría usarse en paralelo. Perfilar el código para ver qué operaciones realmente toman más tiempo sería un buen próximo paso si desea ir aún más rápido.

Gregor Thomas
fuente
1
Creo que esto terminará más rápido que aplicar y no dependerá de eliminar valores vacíos si tienen longitudes diferentes. ¿podría sugerir un bucle envuelto en unlist?
Carl Boneri
1
Single loop mejora la velocidad un poco más, ¡gracias!
Gregor Thomas el
si hombre. Sin embargo, esto todavía me está molestando. Siento que me estoy perdiendo una manera muy obvia y más fácil de hacer todo esto ...
Carl Boneri
Quiero decir, probablemente sería bastante fácil escribir una función RCPP que simplemente haga esto y sea increíblemente rápido. Pero trabajando dentro de R, creo que estamos limitados a hacer básicamente estos pasos.
Gregor Thomas
eso es lo que estaba pensando: C ++
Carl Boneri
1

Esta versión es un poco más 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])
}

Pero creo que podría estar optimizado.

Félix Cuneo
fuente
Solo funcionará si la longitud de todas las cadenas es la misma. ¡Agradable y rápido, sin embargo!
Gregor Thomas