Comment trier efficacement les caractères d'une chaîne en R?

9

Comment puis-je trier efficacement les caractères de chaque chaîne d'un vecteur? Par exemple, étant donné un vecteur de chaînes:

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

J'ai écrit une fonction qui divisera chaque chaîne en un vecteur, triera le vecteur, puis réduira la sortie:

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)

Cependant, le vecteur de chaînes auquel je dois l'appliquer est très long et cette fonction est trop lente. Quelqu'un a-t-il des suggestions pour améliorer les performances?

Powege
la source
1
Découvrez le package stringi - il offre une accélération par rapport à la base. La réponse de Rich Scriven donne plus de détails: stackoverflow.com/questions/5904797/…
user2474226
Ils lettersne sont pas toujours de longueur trois comme dans votre exemple, n'est-ce pas?
jay.sf
Non, la longueur des cordes peut varier.
Powege
Je pense que l' ajout fixed = TRUEdans strsplit()peut améliorer les performances car il ne comportera pas l'utilisation de regex.
tmfmnk

Réponses:

3

Vous pouvez réduire le temps en minimisant le nombre de boucles à coup sûr, et le faire en utilisant le parallelpackage ... mon approche serait de fractionner les chaînes une fois, puis de trier et coller les boucles:

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 rase en 4 secondes, mais ce n'est toujours pas si rapide ...

Éditer

Ok, je l'ai bien compris en utilisant apply.. la stratégie ici:

1) extraire des lettres plutôt que de séparer les limites 2) créer une matrice avec les résultats 3) parcourir les lignes 4) trier 5) joindre

Vous évitez les boucles multiples et la suppression de la liste .... IGNORE:? Mise en garde est que si les chaînes de différentes longueurs, vous devrez supprimer tout vide ou NA dans le applytel quei[!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

Cela nous prend de 10,3 secondes à 3,98

Carl Boneri
la source
Quelle est l'accélération si vous exécutez la fonction d'origine en parallèle?
slava-kohut
baissé d'un peu plus de 50%. tmp <- strsplit(strings, split="") unlist(mclapply(tmp, function(i){ paste0(sort(i), collapse = "") }))
Carl Boneri
@Gregor c'est le cas. Vous venez de tester et semble?
Carl Boneri
Cool, je vérifie juste :)
Gregor Thomas
Non pas du tout .. j'ai eu totalement la même question moi-même .. ce qui signifie omettre la note que j'ai mise dans la réponse concernant la suppression de NA / vide ... je n'en ai pas besoin. stringiest de loin mon paquet préféré ...
Carl Boneri
4

La réimplémentation à l'aide stringidonne une accélération d'environ 4x. J'ai également édité sort_catpour l'utiliser fixed = TRUEdans le strsplit, ce qui le rend un peu plus rapide. Et merci à Carl pour la suggestion de boucle unique, qui nous accélère un peu plus.

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>

Cette méthode pourrait également être utilisée en parallèle. Le profilage du code pour voir quelles opérations prennent réellement le plus de temps serait une bonne étape suivante si vous voulez aller encore plus vite.

Gregor Thomas
la source
1
Je pense que cela se terminera plus rapidement qu'appliquer et ne reposera pas sur la suppression de valeurs vides si différentes longueurs. pourrait suggérer une boucle enveloppée dans unlist, cependant?
Carl Boneri
1
Une boucle unique améliore la vitesse un peu plus, merci!
Gregor Thomas
oui l'homme. cela me dérange toujours, cependant. J'ai l'impression qu'il me manque un moyen très évident et plus facile de faire tout cela ....
Carl Boneri
Je veux dire, il serait probablement assez facile d'écrire une fonction RCPP qui fait juste cela et serait rapide comme l'éclair. Mais en travaillant avec R, je pense que nous sommes limités à faire essentiellement ces étapes.
Gregor Thomas
c'est ce que je pensais: C ++
Carl Boneri
1

Cette version est légèrement plus rapide

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

Mais je pense que ça pourrait être optimisé

Félix Cuneo
la source
Ne fonctionnera que si la longueur de toutes les chaînes est la même. Agréable et rapide!
Gregor Thomas